@fleetless/sdk 4.2.0 → 4.4.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/index.cjs +1513 -384
- package/dist/index.d.cts +238 -32
- package/dist/index.d.ts +238 -32
- package/dist/index.js +1513 -384
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -243,7 +243,7 @@ function isRenderKind(kind) {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
async function forEachWithConcurrency(items,
|
|
246
|
+
async function forEachWithConcurrency(items, limit2, fn) {
|
|
247
247
|
let next = 0;
|
|
248
248
|
let failed = false;
|
|
249
249
|
let firstError;
|
|
@@ -261,7 +261,7 @@ async function forEachWithConcurrency(items, limit, fn) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
await Promise.all(Array.from({ length: Math.min(
|
|
264
|
+
await Promise.all(Array.from({ length: Math.min(limit2, items.length) }, () => worker()));
|
|
265
265
|
if (failed) throw firstError;
|
|
266
266
|
}
|
|
267
267
|
function createAssetsApi(http) {
|
|
@@ -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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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.2.0-next.2/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({
|
|
@@ -4160,7 +4358,8 @@ var clientOidcErrorCode = z13.enum([
|
|
|
4160
4358
|
"provider_misconfigured",
|
|
4161
4359
|
"provider_disabled",
|
|
4162
4360
|
"invalid_request",
|
|
4163
|
-
"quota_exceeded"
|
|
4361
|
+
"quota_exceeded",
|
|
4362
|
+
"plan_limit"
|
|
4164
4363
|
]);
|
|
4165
4364
|
var clientMcpInteraction = z13.object({
|
|
4166
4365
|
id: z13.string().meta({ description: "The interaction, as it arrived in the app's `mcp_login_url`. Not a credential: it names a pending request the server already holds, and approving it needs the app user's own access token." }),
|
|
@@ -4218,10 +4417,13 @@ var clientIdentity = z13.object({
|
|
|
4218
4417
|
}),
|
|
4219
4418
|
email: z13.email().nullable().meta({
|
|
4220
4419
|
description: "The address of the Fleetless user or app user behind this session, and `null` for a server key, which is not a person."
|
|
4420
|
+
}),
|
|
4421
|
+
two_factor_enabled: z13.boolean().nullable().meta({
|
|
4422
|
+
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
4423
|
})
|
|
4222
4424
|
});
|
|
4223
4425
|
|
|
4224
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4426
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/realtime.js
|
|
4225
4427
|
var clientAuth = z14.object({
|
|
4226
4428
|
type: z14.literal("auth"),
|
|
4227
4429
|
token: z14.string().min(1)
|
|
@@ -4389,6 +4591,12 @@ var liveSessionEndReason = z14.enum([
|
|
|
4389
4591
|
"expired",
|
|
4390
4592
|
/** The robot was deleted out from under the session. */
|
|
4391
4593
|
"robot_deleted",
|
|
4594
|
+
/**
|
|
4595
|
+
* The organization's live video for app users reached its plan's monthly
|
|
4596
|
+
* limit (2026-10-02, fleetless/fleetless#103); `detail` carries the
|
|
4597
|
+
* sentence the viewer shows.
|
|
4598
|
+
*/
|
|
4599
|
+
"plan_limit",
|
|
4392
4600
|
/**
|
|
4393
4601
|
* The cloud ended it and cannot say which of the above applied. **Kept
|
|
4394
4602
|
* deliberately**: a channel that cannot say "I do not know" will say
|
|
@@ -4499,34 +4707,59 @@ var orgEventDropped = z14.object({
|
|
|
4499
4707
|
dropped: z14.number().int().positive()
|
|
4500
4708
|
}).strict();
|
|
4501
4709
|
|
|
4502
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4710
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/feedback.js
|
|
4503
4711
|
import { z as z15 } from "zod";
|
|
4504
|
-
var
|
|
4712
|
+
var FEEDBACK_KINDS = ["idea", "problem", "question", "other"];
|
|
4713
|
+
var feedbackKind = z15.enum(FEEDBACK_KINDS);
|
|
4714
|
+
var FEEDBACK_MESSAGE_MAX = 5e3;
|
|
4715
|
+
var feedbackRequest = z15.object({
|
|
4716
|
+
kind: feedbackKind.meta({
|
|
4717
|
+
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."
|
|
4718
|
+
}),
|
|
4719
|
+
message: z15.string().trim().min(1).max(FEEDBACK_MESSAGE_MAX).meta({
|
|
4720
|
+
description: "What the developer wrote, trimmed. At most 5000 characters; a message that is only whitespace is refused."
|
|
4721
|
+
}),
|
|
4722
|
+
page: z15.string().startsWith("/").max(512).meta({
|
|
4723
|
+
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."
|
|
4724
|
+
})
|
|
4725
|
+
}).strict();
|
|
4726
|
+
var feedbackResponse = z15.object({
|
|
4727
|
+
id: z15.uuid().meta({
|
|
4728
|
+
description: "The stored message. It exists whatever `mail` says."
|
|
4729
|
+
}),
|
|
4730
|
+
mail: mailStatus.meta({
|
|
4731
|
+
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."
|
|
4732
|
+
})
|
|
4733
|
+
}).strict();
|
|
4734
|
+
|
|
4735
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/client-robots.js
|
|
4736
|
+
import { z as z16 } from "zod";
|
|
4737
|
+
var clientRobotListItem = z16.object({
|
|
4505
4738
|
...robot.shape,
|
|
4506
4739
|
bridge_state: bridgeState.meta({
|
|
4507
4740
|
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
4741
|
}),
|
|
4509
|
-
published_version:
|
|
4742
|
+
published_version: z16.number().int().positive().nullable().meta({
|
|
4510
4743
|
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
4744
|
})
|
|
4512
4745
|
});
|
|
4513
|
-
var clientRobotListResponse =
|
|
4514
|
-
robots:
|
|
4746
|
+
var clientRobotListResponse = z16.object({
|
|
4747
|
+
robots: z16.array(clientRobotListItem).meta({
|
|
4515
4748
|
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
4749
|
})
|
|
4517
4750
|
});
|
|
4518
4751
|
|
|
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:
|
|
4752
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/audit.js
|
|
4753
|
+
import { z as z17 } from "zod";
|
|
4754
|
+
var auditActor = z17.object({
|
|
4755
|
+
kind: z17.enum(["developer", "end_user", "app_user", "server_key", "bridge", "fleetless"]),
|
|
4756
|
+
id: z17.uuid(),
|
|
4757
|
+
label: z17.string().min(1).max(200)
|
|
4758
|
+
});
|
|
4759
|
+
var auditEvent = z17.object({
|
|
4760
|
+
id: z17.uuid(),
|
|
4761
|
+
org_id: z17.uuid(),
|
|
4762
|
+
at: z17.iso.datetime(),
|
|
4530
4763
|
/**
|
|
4531
4764
|
* A monotonic counter, ascending in write order, unique across the log.
|
|
4532
4765
|
*
|
|
@@ -4545,18 +4778,18 @@ var auditEvent = z16.object({
|
|
|
4545
4778
|
* Required, not optional: an event without a sequence cannot be ordered
|
|
4546
4779
|
* against one that has it, and a log with two orderings has none.
|
|
4547
4780
|
*/
|
|
4548
|
-
seq:
|
|
4781
|
+
seq: z17.number().int().positive(),
|
|
4549
4782
|
actor: auditActor,
|
|
4550
4783
|
/** Stable dotted name, e.g. `app_user.login`, `config.published`. */
|
|
4551
|
-
action:
|
|
4784
|
+
action: z17.string().min(1).max(80),
|
|
4552
4785
|
/**
|
|
4553
4786
|
* What the action was about, if anything — a robot, an app, a user. Free
|
|
4554
4787
|
* of ids the console cannot resolve: carry the label with it.
|
|
4555
4788
|
*/
|
|
4556
|
-
target:
|
|
4557
|
-
kind:
|
|
4558
|
-
id:
|
|
4559
|
-
label:
|
|
4789
|
+
target: z17.object({
|
|
4790
|
+
kind: z17.string().min(1).max(40),
|
|
4791
|
+
id: z17.string().min(1),
|
|
4792
|
+
label: z17.string().min(1).max(200)
|
|
4560
4793
|
}).nullable(),
|
|
4561
4794
|
/**
|
|
4562
4795
|
* Action-specific extras.
|
|
@@ -4570,10 +4803,10 @@ var auditEvent = z16.object({
|
|
|
4570
4803
|
* So: never credentials, never tokens. That is a rule, not a guarantee the
|
|
4571
4804
|
* schema enforces.
|
|
4572
4805
|
*/
|
|
4573
|
-
details:
|
|
4806
|
+
details: z17.record(z17.string(), z17.unknown()).nullable()
|
|
4574
4807
|
});
|
|
4575
4808
|
var auditTimestampMs = wireTimestampMs;
|
|
4576
|
-
var auditQuery =
|
|
4809
|
+
var auditQuery = z17.object({
|
|
4577
4810
|
/** Only events with a smaller `seq` — the next, older page. */
|
|
4578
4811
|
before_seq: wireSeqCursor.optional(),
|
|
4579
4812
|
/**
|
|
@@ -4582,9 +4815,9 @@ var auditQuery = z16.object({
|
|
|
4582
4815
|
* coercion's result in either `io` direction, so the artifact would describe
|
|
4583
4816
|
* a shape a query string can never carry.
|
|
4584
4817
|
*/
|
|
4585
|
-
limit:
|
|
4818
|
+
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
4819
|
/** Exact action name, e.g. `config.published`. No prefix matching: a filter that matches more than it says is not one. */
|
|
4587
|
-
action:
|
|
4820
|
+
action: z17.string().min(1).max(80).optional(),
|
|
4588
4821
|
/**
|
|
4589
4822
|
* Everything under a dotted prefix, e.g. `server_key.` for all three
|
|
4590
4823
|
* server-key actions.
|
|
@@ -4601,7 +4834,7 @@ var auditQuery = z16.object({
|
|
|
4601
4834
|
* happily. The cloud is the only enforcement point — the same residual
|
|
4602
4835
|
* `orgLatencyQuery` and `orgUsageQuery` already name.
|
|
4603
4836
|
*/
|
|
4604
|
-
action_prefix:
|
|
4837
|
+
action_prefix: z17.string().min(1).max(80).optional(),
|
|
4605
4838
|
/**
|
|
4606
4839
|
* Only events by this actor.
|
|
4607
4840
|
*
|
|
@@ -4613,9 +4846,21 @@ var auditQuery = z16.object({
|
|
|
4613
4846
|
* Not an injection question — the query is parameterised either way. It is a
|
|
4614
4847
|
* **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
|
|
4615
4848
|
*/
|
|
4616
|
-
actor_id:
|
|
4849
|
+
actor_id: z17.uuid().optional(),
|
|
4617
4850
|
/** Only events about this kind of target, e.g. `robot`. */
|
|
4618
|
-
target_kind:
|
|
4851
|
+
target_kind: z17.string().min(1).max(40).optional(),
|
|
4852
|
+
/**
|
|
4853
|
+
* Only events about this target — and, for a robot, also the events that
|
|
4854
|
+
* name it in `details.robot_id`, so a robot's log includes what was
|
|
4855
|
+
* started on it.
|
|
4856
|
+
*
|
|
4857
|
+
* **A string, not `z.uuid()`**, unlike `actor_id` above: `target.id` is a
|
|
4858
|
+
* string in this contract and text in the cloud's table, so a uuid rule
|
|
4859
|
+
* here would refuse ids the log can hold, and no value can fail a cast.
|
|
4860
|
+
*/
|
|
4861
|
+
target_id: z17.string().min(1).max(200).optional().meta({
|
|
4862
|
+
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."
|
|
4863
|
+
}),
|
|
4619
4864
|
/**
|
|
4620
4865
|
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
4621
4866
|
* same rule the history shapes follow.
|
|
@@ -4631,8 +4876,8 @@ var auditQuery = z16.object({
|
|
|
4631
4876
|
message: "action and action_prefix cannot be combined",
|
|
4632
4877
|
path: ["action_prefix"]
|
|
4633
4878
|
});
|
|
4634
|
-
var auditListResponse =
|
|
4635
|
-
events:
|
|
4879
|
+
var auditListResponse = z17.object({
|
|
4880
|
+
events: z17.array(auditEvent),
|
|
4636
4881
|
/**
|
|
4637
4882
|
* The `seq` a caller sends as `before_seq` to keep reading — or `null` when
|
|
4638
4883
|
* there is nothing further.
|
|
@@ -4643,32 +4888,361 @@ var auditListResponse = z16.object({
|
|
|
4643
4888
|
* not mean *no more* here. The same distinction `historySamples` was given
|
|
4644
4889
|
* `truncated` for.
|
|
4645
4890
|
*/
|
|
4646
|
-
next_cursor:
|
|
4891
|
+
next_cursor: z17.number().int().positive().nullable()
|
|
4647
4892
|
});
|
|
4648
4893
|
|
|
4649
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4650
|
-
import { z as
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4894
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/errors.js
|
|
4895
|
+
import { z as z19 } from "zod";
|
|
4896
|
+
|
|
4897
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/plans.js
|
|
4898
|
+
import { z as z18 } from "zod";
|
|
4899
|
+
var planId = z18.enum(["basic", "plus", "pro", "enterprise"]);
|
|
4900
|
+
var planLimitKey = z18.enum([
|
|
4901
|
+
"seats",
|
|
4902
|
+
"robots",
|
|
4903
|
+
"apps",
|
|
4904
|
+
"app_users",
|
|
4905
|
+
"live_video_ms_per_month",
|
|
4906
|
+
"asset_bytes_per_robot",
|
|
4907
|
+
"history_days",
|
|
4908
|
+
"audit_days"
|
|
4909
|
+
]);
|
|
4910
|
+
var limit = z18.number().int().positive().nullable();
|
|
4911
|
+
var planLimits = z18.object({
|
|
4912
|
+
seats: limit.meta({ description: "Developers, owners included, plus pending team invitations." }),
|
|
4913
|
+
robots: limit.meta({ description: "Robots in the org." }),
|
|
4914
|
+
apps: limit.meta({ description: "Apps in the org." }),
|
|
4915
|
+
app_users: limit.meta({ description: "App users across every app of the org, plus pending app-user invitations." }),
|
|
4916
|
+
live_video_ms_per_month: limit.meta({
|
|
4917
|
+
description: "Live video watched by app users in one UTC calendar month, in milliseconds, across the org. Console sessions do not count."
|
|
4918
|
+
}),
|
|
4919
|
+
asset_bytes_per_robot: limit.meta({
|
|
4920
|
+
description: "Asset storage per robot, in bytes (decimal: 1 GB = 1,000,000,000). The org stores up to robots \xD7 this value in total."
|
|
4921
|
+
}),
|
|
4922
|
+
history_days: limit.meta({ description: "Days the org's robot history is kept." }),
|
|
4923
|
+
audit_days: limit.meta({ description: "Days the org's audit log is kept." })
|
|
4924
|
+
});
|
|
4925
|
+
var planFeature = z18.enum(["app_mcp", "two_factor", "require_two_factor", "app_oidc", "hosted_logo", "audit_export", "addons"]);
|
|
4926
|
+
var FEATURE_DESCRIPTIONS = {
|
|
4927
|
+
app_mcp: "An app's own MCP endpoint, for its app users.",
|
|
4928
|
+
two_factor: "Two-factor sign-in for developers.",
|
|
4929
|
+
require_two_factor: "An owner may require two-factor sign-in for every developer of the org.",
|
|
4930
|
+
app_oidc: "An app may let its users sign in through an OpenID Connect identity provider.",
|
|
4931
|
+
hosted_logo: "An app's hosted pages show its logo and accent colour, not only its name.",
|
|
4932
|
+
audit_export: "The audit log can be exported as CSV.",
|
|
4933
|
+
addons: "Add-ons can be bought on top of the plan."
|
|
4934
|
+
};
|
|
4935
|
+
var planFeatures = z18.object(Object.fromEntries(planFeature.options.map((f) => [f, z18.boolean().meta({ description: FEATURE_DESCRIPTIONS[f] })])));
|
|
4936
|
+
var cents = z18.number().int().nonnegative();
|
|
4937
|
+
var planPrices = z18.object({
|
|
4938
|
+
eur_month: cents.meta({ description: "Per month in euro cents, excluding VAT." }),
|
|
4939
|
+
usd_month: cents.meta({ description: "Per month in US dollar cents, excluding VAT: the euro price \xD7 1.15, rounded up to a whole dollar." }),
|
|
4940
|
+
eur_year: cents.meta({ description: "Per year in euro cents, excluding VAT: twelve months less 15 %." }),
|
|
4941
|
+
usd_year: cents.meta({ description: "Per year in US dollar cents, excluding VAT: the yearly euro price \xD7 1.15, rounded up to a whole dollar." })
|
|
4942
|
+
});
|
|
4943
|
+
var planSupport = z18.enum(["community", "email", "priority", "named_contact"]);
|
|
4944
|
+
var planCatalogueEntry = z18.object({
|
|
4945
|
+
id: planId.meta({ description: "The plan." }),
|
|
4946
|
+
name: z18.string().min(1).meta({ description: "The plan's display name: Basic, Plus, Pro or Enterprise." }),
|
|
4947
|
+
limits: planLimits.meta({ description: "What the plan allows. `null` means by contract." }),
|
|
4948
|
+
features: planFeatures.meta({ description: "What the plan unlocks." }),
|
|
4949
|
+
prices: planPrices.nullable().meta({ description: "`null` for Enterprise: sold by contract, on request." }),
|
|
4950
|
+
support: planSupport.meta({ description: "The support that comes with the plan." })
|
|
4951
|
+
});
|
|
4952
|
+
var addonKey = z18.enum(["seats", "robots", "apps", "app_user_packs", "live_video_packs"]);
|
|
4953
|
+
var addonCatalogueEntry = z18.object({
|
|
4954
|
+
key: addonKey.meta({ description: "The add-on." }),
|
|
4955
|
+
raises: planLimitKey.meta({ description: "The plan limit this add-on raises." }),
|
|
4956
|
+
per_unit: z18.number().int().positive().meta({ description: "How much one unit of this add-on raises `raises` by." }),
|
|
4957
|
+
prices: planPrices.meta({ description: "The price of one unit." })
|
|
4958
|
+
});
|
|
4959
|
+
function usdCentsFromEurCents(eurCents) {
|
|
4960
|
+
return Math.floor((eurCents * 115 + 9999) / 1e4) * 100;
|
|
4961
|
+
}
|
|
4962
|
+
function yearlyEurCents(monthEurCents) {
|
|
4963
|
+
return Math.round(monthEurCents * 12 * 85 / 100);
|
|
4964
|
+
}
|
|
4965
|
+
function pricesFromEurMonth(eurMonthCents) {
|
|
4966
|
+
const eurYear = yearlyEurCents(eurMonthCents);
|
|
4967
|
+
return {
|
|
4968
|
+
eur_month: eurMonthCents,
|
|
4969
|
+
usd_month: usdCentsFromEurCents(eurMonthCents),
|
|
4970
|
+
eur_year: eurYear,
|
|
4971
|
+
usd_year: usdCentsFromEurCents(eurYear)
|
|
4972
|
+
};
|
|
4973
|
+
}
|
|
4974
|
+
var PLANS = {
|
|
4975
|
+
basic: {
|
|
4976
|
+
id: "basic",
|
|
4977
|
+
name: "Basic",
|
|
4978
|
+
limits: {
|
|
4979
|
+
seats: 1,
|
|
4980
|
+
robots: 1,
|
|
4981
|
+
apps: 1,
|
|
4982
|
+
app_users: 10,
|
|
4983
|
+
live_video_ms_per_month: 36e6,
|
|
4984
|
+
asset_bytes_per_robot: 1e9,
|
|
4985
|
+
history_days: 7,
|
|
4986
|
+
audit_days: 7
|
|
4987
|
+
},
|
|
4988
|
+
features: {
|
|
4989
|
+
app_mcp: false,
|
|
4990
|
+
two_factor: false,
|
|
4991
|
+
require_two_factor: false,
|
|
4992
|
+
app_oidc: false,
|
|
4993
|
+
hosted_logo: false,
|
|
4994
|
+
audit_export: false,
|
|
4995
|
+
addons: false
|
|
4996
|
+
},
|
|
4997
|
+
prices: pricesFromEurMonth(0),
|
|
4998
|
+
support: "community"
|
|
4999
|
+
},
|
|
5000
|
+
plus: {
|
|
5001
|
+
id: "plus",
|
|
5002
|
+
name: "Plus",
|
|
5003
|
+
limits: {
|
|
5004
|
+
seats: 3,
|
|
5005
|
+
robots: 3,
|
|
5006
|
+
apps: 3,
|
|
5007
|
+
app_users: 25,
|
|
5008
|
+
live_video_ms_per_month: 36e7,
|
|
5009
|
+
asset_bytes_per_robot: 2e9,
|
|
5010
|
+
history_days: 30,
|
|
5011
|
+
audit_days: 30
|
|
5012
|
+
},
|
|
5013
|
+
features: {
|
|
5014
|
+
app_mcp: true,
|
|
5015
|
+
two_factor: true,
|
|
5016
|
+
require_two_factor: false,
|
|
5017
|
+
app_oidc: true,
|
|
5018
|
+
hosted_logo: true,
|
|
5019
|
+
audit_export: false,
|
|
5020
|
+
addons: false
|
|
5021
|
+
},
|
|
5022
|
+
prices: pricesFromEurMonth(2900),
|
|
5023
|
+
support: "email"
|
|
5024
|
+
},
|
|
5025
|
+
pro: {
|
|
5026
|
+
id: "pro",
|
|
5027
|
+
name: "Pro",
|
|
5028
|
+
limits: {
|
|
5029
|
+
seats: 5,
|
|
5030
|
+
robots: 5,
|
|
5031
|
+
apps: 5,
|
|
5032
|
+
app_users: 50,
|
|
5033
|
+
live_video_ms_per_month: 9e8,
|
|
5034
|
+
asset_bytes_per_robot: 3e9,
|
|
5035
|
+
history_days: 90,
|
|
5036
|
+
audit_days: 90
|
|
5037
|
+
},
|
|
5038
|
+
features: {
|
|
5039
|
+
app_mcp: true,
|
|
5040
|
+
two_factor: true,
|
|
5041
|
+
require_two_factor: true,
|
|
5042
|
+
app_oidc: true,
|
|
5043
|
+
hosted_logo: true,
|
|
5044
|
+
audit_export: true,
|
|
5045
|
+
addons: true
|
|
5046
|
+
},
|
|
5047
|
+
prices: pricesFromEurMonth(14900),
|
|
5048
|
+
support: "priority"
|
|
5049
|
+
},
|
|
5050
|
+
enterprise: {
|
|
5051
|
+
id: "enterprise",
|
|
5052
|
+
name: "Enterprise",
|
|
5053
|
+
limits: {
|
|
5054
|
+
seats: null,
|
|
5055
|
+
robots: null,
|
|
5056
|
+
apps: null,
|
|
5057
|
+
app_users: null,
|
|
5058
|
+
live_video_ms_per_month: null,
|
|
5059
|
+
asset_bytes_per_robot: null,
|
|
5060
|
+
history_days: null,
|
|
5061
|
+
audit_days: null
|
|
5062
|
+
},
|
|
5063
|
+
features: {
|
|
5064
|
+
app_mcp: true,
|
|
5065
|
+
two_factor: true,
|
|
5066
|
+
require_two_factor: true,
|
|
5067
|
+
app_oidc: true,
|
|
5068
|
+
hosted_logo: true,
|
|
5069
|
+
audit_export: true,
|
|
5070
|
+
addons: true
|
|
5071
|
+
},
|
|
5072
|
+
prices: null,
|
|
5073
|
+
support: "named_contact"
|
|
5074
|
+
}
|
|
5075
|
+
};
|
|
5076
|
+
var ADDONS = {
|
|
5077
|
+
seats: { key: "seats", raises: "seats", per_unit: 1, prices: pricesFromEurMonth(900) },
|
|
5078
|
+
robots: { key: "robots", raises: "robots", per_unit: 1, prices: pricesFromEurMonth(1900) },
|
|
5079
|
+
apps: { key: "apps", raises: "apps", per_unit: 1, prices: pricesFromEurMonth(900) },
|
|
5080
|
+
app_user_packs: { key: "app_user_packs", raises: "app_users", per_unit: 5, prices: pricesFromEurMonth(1e3) },
|
|
5081
|
+
live_video_packs: { key: "live_video_packs", raises: "live_video_ms_per_month", per_unit: 9e8, prices: pricesFromEurMonth(900) }
|
|
5082
|
+
};
|
|
5083
|
+
var planCurrency = z18.enum(["eur", "usd"]);
|
|
5084
|
+
var addonCount = z18.number().int().nonnegative();
|
|
5085
|
+
var orgAddons = z18.object({
|
|
5086
|
+
seats: addonCount.meta({ description: "Extra developer seats, one each." }),
|
|
5087
|
+
robots: addonCount.meta({ description: "Extra robots, one each." }),
|
|
5088
|
+
apps: addonCount.meta({ description: "Extra apps, one each." }),
|
|
5089
|
+
app_user_packs: addonCount.meta({ description: "Packs of five extra app users." }),
|
|
5090
|
+
live_video_packs: addonCount.meta({ description: "Packs of 250 extra hours of app-user live video per month." })
|
|
5091
|
+
});
|
|
5092
|
+
var usageCount = z18.number().int().nonnegative();
|
|
5093
|
+
var orgPlanUsage = z18.object({
|
|
5094
|
+
seats: usageCount.meta({ description: "Developers, owners included, plus pending team invitations." }),
|
|
5095
|
+
robots: usageCount.meta({ description: "Robots in the org." }),
|
|
5096
|
+
apps: usageCount.meta({ description: "Apps in the org." }),
|
|
5097
|
+
app_users: usageCount.meta({ description: "App users across every app, plus pending app-user invitations." }),
|
|
5098
|
+
live_video_ms_this_month: usageCount.meta({
|
|
5099
|
+
description: "Live video watched by app users in the current UTC calendar month, in milliseconds. Console sessions do not count."
|
|
5100
|
+
}),
|
|
5101
|
+
asset_bytes: usageCount.meta({ description: "Bytes the assets of every robot of the org occupy, together." })
|
|
5102
|
+
});
|
|
5103
|
+
var planChangeKeep = z18.object({
|
|
5104
|
+
robots: z18.array(z18.uuid()).meta({ description: "The robots that stay, by id. Every other robot is deleted when the change takes effect." }),
|
|
5105
|
+
apps: z18.array(z18.uuid()).meta({ description: "The apps that stay, by id. Every other app is deleted when the change takes effect." }),
|
|
5106
|
+
app_users: z18.array(z18.uuid()).meta({
|
|
5107
|
+
description: "The app users that stay, by id, across every app. Every other app user is deleted when the change takes effect."
|
|
5108
|
+
}),
|
|
5109
|
+
developers: z18.array(z18.uuid()).meta({
|
|
5110
|
+
description: "The developers that stay, by user id, owners never among them: every owner stays. Every other developer is removed from the org when the change takes effect."
|
|
5111
|
+
})
|
|
5112
|
+
}).strict();
|
|
5113
|
+
var planChangeReason = z18.enum(["downgrade", "cancel", "migration", "lock"]);
|
|
5114
|
+
var pendingPlanChange = z18.object({
|
|
5115
|
+
target_plan: planId.meta({ description: "The plan the org moves to." }),
|
|
5116
|
+
reason: planChangeReason.meta({ description: "Why the change is pending: `downgrade`, `cancel`, `migration` or `lock`." }),
|
|
5117
|
+
effective_at: z18.iso.datetime().nullable().meta({
|
|
5118
|
+
description: "When the change takes effect. `null` only for a move off the beta whose date is not set yet."
|
|
5119
|
+
}),
|
|
5120
|
+
keep: planChangeKeep.nullable().meta({
|
|
5121
|
+
description: "What stays. `null` when the org already fits the target plan and nothing is deleted."
|
|
5122
|
+
}),
|
|
5123
|
+
history_days_after: z18.number().int().positive().meta({
|
|
5124
|
+
description: "Days of history and audit log the org keeps on the target plan."
|
|
5125
|
+
}),
|
|
5126
|
+
chosen_by: z18.uuid().meta({ description: "The user id of the owner who chose the change, or the nil UUID when Fleetless queued it." }),
|
|
5127
|
+
chosen_at: z18.iso.datetime().meta({ description: "When the change was chosen." })
|
|
5128
|
+
});
|
|
5129
|
+
var orgLock = z18.object({
|
|
5130
|
+
reason: z18.enum(["payment", "migration"]).meta({
|
|
5131
|
+
description: "`payment`: a payment is missing. `migration`: the org did not choose what stays when the beta ended."
|
|
5132
|
+
}),
|
|
5133
|
+
since: z18.iso.datetime().meta({ description: "When the org was locked." })
|
|
5134
|
+
});
|
|
5135
|
+
var orgPlan = z18.object({
|
|
5136
|
+
plan: planId.meta({ description: "The org's current plan." }),
|
|
5137
|
+
currency: planCurrency.meta({ description: "The currency the org's prices are shown and billed in." }),
|
|
5138
|
+
period_ends_at: z18.iso.datetime().meta({
|
|
5139
|
+
description: "The end of the organization's current billing period; while no payment period exists yet, the end of the current UTC calendar month. A pending downward plan change normally takes effect at this exact instant \u2014 except one chosen while the org was locked, which lands at once, and the platform's own move off the beta, which lands at its switch date instead."
|
|
5140
|
+
}),
|
|
5141
|
+
addons: orgAddons.meta({ description: "The add-ons the org has bought. All zero on a plan without the `addons` feature." }),
|
|
5142
|
+
limits: planLimits.meta({
|
|
5143
|
+
description: "The org's effective limits: the plan's, raised by its add-ons, or an operator's override in their place. `null` means unlimited for a count, and 90 days for `history_days` and `audit_days`."
|
|
5144
|
+
}),
|
|
5145
|
+
features: planFeatures.meta({ description: "What the org's plan unlocks." }),
|
|
5146
|
+
usage: orgPlanUsage.meta({ description: "What the org uses now, counted the way each limit counts it." }),
|
|
5147
|
+
pending_change: pendingPlanChange.nullable().meta({ description: "A move to a lower plan that has not taken effect yet, or `null`." }),
|
|
5148
|
+
lock: orgLock.nullable().meta({ description: "Why and since when the org is locked, or `null` when it is not." }),
|
|
5149
|
+
switch: z18.object({
|
|
5150
|
+
at: z18.iso.datetime().nullable().meta({
|
|
5151
|
+
description: "When the org moves from the beta onto its plan. `null` while the date is not set."
|
|
5152
|
+
}),
|
|
5153
|
+
needs_choice: z18.boolean().meta({
|
|
5154
|
+
description: "Whether the org uses more than Basic allows, so an owner has to choose what stays before `at`."
|
|
5155
|
+
})
|
|
5156
|
+
}).nullable().meta({ description: "Set only while the org is still on the beta; `null` for every other org." })
|
|
4655
5157
|
});
|
|
4656
|
-
var
|
|
4657
|
-
|
|
5158
|
+
var planChangeRequest = z18.object({
|
|
5159
|
+
target_plan: planId.meta({ description: "The lower plan to move to; `basic` cancels." }),
|
|
5160
|
+
keep: planChangeKeep.nullable().meta({
|
|
5161
|
+
description: "What stays. `null` when the org already fits the target plan, so nothing is deleted."
|
|
5162
|
+
})
|
|
5163
|
+
}).strict();
|
|
5164
|
+
var planOverrides = z18.object(Object.fromEntries(planLimitKey.options.map((k) => [
|
|
5165
|
+
k,
|
|
5166
|
+
limit.optional().meta({ description: `Replaces the plan's \`${k}\`. \`null\` clears the override.` })
|
|
5167
|
+
]))).strict();
|
|
5168
|
+
var adminPlanChangeRequest = z18.object({
|
|
5169
|
+
plan: planId.meta({ description: "The plan the org is on after this request." }),
|
|
5170
|
+
addons: orgAddons.partial().strict().optional().meta({ description: "Add-on counts to set. Counts not named stay as they are." }),
|
|
5171
|
+
overrides: planOverrides.optional().meta({ description: "Limits to override. `null` clears an override." }),
|
|
5172
|
+
currency: planCurrency.optional().meta({ description: "The currency the org is billed in." }),
|
|
5173
|
+
period_ends_at: z18.iso.datetime().nullable().optional().meta({
|
|
5174
|
+
description: "The end of the org's billing period. `null` falls back to the end of the current UTC month."
|
|
5175
|
+
})
|
|
5176
|
+
}).strict();
|
|
5177
|
+
|
|
5178
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/errors.js
|
|
5179
|
+
var apiError = z19.object({
|
|
5180
|
+
code: z19.string().min(1),
|
|
5181
|
+
message: z19.string().min(1),
|
|
5182
|
+
details: z19.unknown().optional()
|
|
5183
|
+
});
|
|
5184
|
+
var parameterViolation = z19.object({
|
|
5185
|
+
field: z19.string().min(1),
|
|
4658
5186
|
/** Which rule failed — `min`, `max`, `enum`, `pattern`, `required`, `undeclared`. */
|
|
4659
|
-
rule:
|
|
4660
|
-
message:
|
|
5187
|
+
rule: z19.string().min(1),
|
|
5188
|
+
message: z19.string().min(1)
|
|
5189
|
+
});
|
|
5190
|
+
var parameterInvalidDetails = z19.object({
|
|
5191
|
+
violations: z19.array(parameterViolation).min(1)
|
|
5192
|
+
});
|
|
5193
|
+
var cancelRejectedDetails = z19.object({
|
|
5194
|
+
goals: z19.array(bridgeCancelResultEntry).min(1)
|
|
5195
|
+
});
|
|
5196
|
+
var invalidCodeDetails = z19.object({
|
|
5197
|
+
attempts_left: z19.number().int().min(0).meta({
|
|
5198
|
+
description: "How many more wrong codes this code or challenge takes before it is spent. `0` means the next attempt answers `410 token_spent`."
|
|
5199
|
+
})
|
|
5200
|
+
});
|
|
5201
|
+
var planLimitDetails = z19.object({
|
|
5202
|
+
limit: planLimitKey.exclude(["history_days", "audit_days"]).meta({ description: "The limit the action would exceed." }),
|
|
5203
|
+
used: z19.number().int().nonnegative().meta({ description: "How much of the limit the org uses now." }),
|
|
5204
|
+
max: z19.number().int().nonnegative().meta({
|
|
5205
|
+
description: "The limit. For `asset_bytes_per_robot`, the org's whole pool: robots \xD7 the plan's bytes per robot."
|
|
5206
|
+
}),
|
|
5207
|
+
plan: planId.meta({ description: "The plan whose limit refused: the target plan while a move to a lower plan is pending." }),
|
|
5208
|
+
lifted_by: z19.object({
|
|
5209
|
+
plan: planId.nullable().meta({ description: "The cheapest higher plan that raises this limit, or `null` when none does." }),
|
|
5210
|
+
addon: addonKey.nullable().meta({
|
|
5211
|
+
description: "The add-on that raises this limit, when the org's plan can buy add-ons; otherwise `null`."
|
|
5212
|
+
})
|
|
5213
|
+
}).meta({ description: "What would lift the limit." })
|
|
5214
|
+
});
|
|
5215
|
+
var assetPlanLimitDetails = planLimitDetails.extend({
|
|
5216
|
+
...assetStoreRefusedDetails.shape,
|
|
5217
|
+
store_bytes: assetStoreRefusedDetails.shape.store_bytes.meta({
|
|
5218
|
+
description: "The org's asset pool, in bytes: `robots \xD7 asset_bytes_per_robot`."
|
|
5219
|
+
}),
|
|
5220
|
+
used_bytes: assetStoreRefusedDetails.shape.used_bytes.meta({
|
|
5221
|
+
description: "Bytes the org's assets occupy, across every robot, before this upload."
|
|
5222
|
+
})
|
|
4661
5223
|
});
|
|
4662
|
-
var
|
|
4663
|
-
|
|
5224
|
+
var planRequiredDetails = z19.object({
|
|
5225
|
+
feature: planFeature.meta({ description: "The feature the action needs." }),
|
|
5226
|
+
plan: planId.meta({ description: "The org's current plan." }),
|
|
5227
|
+
required_plan: planId.meta({ description: "The cheapest plan that has the feature." })
|
|
5228
|
+
});
|
|
5229
|
+
var orgLockedDetails = z19.object({
|
|
5230
|
+
reason: z19.enum(["payment", "migration"]).meta({
|
|
5231
|
+
description: "`payment`: a payment is missing. `migration`: the org did not choose what stays when the beta ended."
|
|
5232
|
+
})
|
|
4664
5233
|
});
|
|
4665
|
-
var
|
|
4666
|
-
|
|
5234
|
+
var fileTooLargeDetails = z19.object({
|
|
5235
|
+
max_bytes: z19.number().int().positive().meta({
|
|
5236
|
+
description: "The most one asset file can be, in bytes: `ASSET_FILE_MAX_BYTES`, the same on every plan."
|
|
5237
|
+
}),
|
|
5238
|
+
size_bytes: z19.number().int().positive().nullable().meta({
|
|
5239
|
+
description: "The refused file's size, in bytes: the announced size, or the bytes that arrived when none (or a false one) was announced; `null` only when the body limit stopped the upload and nobody counted the bytes."
|
|
5240
|
+
})
|
|
4667
5241
|
});
|
|
4668
5242
|
|
|
4669
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4670
|
-
import { z as
|
|
4671
|
-
var oauthErrorCode =
|
|
5243
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/oauth.js
|
|
5244
|
+
import { z as z20 } from "zod";
|
|
5245
|
+
var oauthErrorCode = z20.enum([
|
|
4672
5246
|
"invalid_request",
|
|
4673
5247
|
"invalid_client",
|
|
4674
5248
|
"invalid_grant",
|
|
@@ -4681,11 +5255,11 @@ var oauthErrorCode = z18.enum([
|
|
|
4681
5255
|
/** RFC 8707: the `resource` named is not one this server issues tokens for. */
|
|
4682
5256
|
"invalid_target"
|
|
4683
5257
|
]);
|
|
4684
|
-
var oauthError =
|
|
5258
|
+
var oauthError = z20.object({
|
|
4685
5259
|
error: oauthErrorCode,
|
|
4686
|
-
error_description:
|
|
5260
|
+
error_description: z20.string().min(1).max(500).optional(),
|
|
4687
5261
|
/** Echoed back per RFC 6749 §4.1.2.1 so a client can match the response. */
|
|
4688
|
-
state:
|
|
5262
|
+
state: z20.string().min(1).max(500).optional(),
|
|
4689
5263
|
/**
|
|
4690
5264
|
* **A Fleetless reason carried inside a standard envelope, and it exists
|
|
4691
5265
|
* because the alternative lost a distinction.**
|
|
@@ -4703,9 +5277,9 @@ var oauthError = z18.object({
|
|
|
4703
5277
|
* our own tooling switches on. RFC 6749 §5.2 permits additional members, and
|
|
4704
5278
|
* a client that ignores this one still behaves correctly.
|
|
4705
5279
|
*/
|
|
4706
|
-
fleetless_code:
|
|
5280
|
+
fleetless_code: z20.string().min(1).max(60).optional()
|
|
4707
5281
|
});
|
|
4708
|
-
var redirectUri =
|
|
5282
|
+
var redirectUri = z20.string().min(1).max(2e3).refine((v) => {
|
|
4709
5283
|
let url;
|
|
4710
5284
|
try {
|
|
4711
5285
|
url = new URL(v);
|
|
@@ -4720,180 +5294,180 @@ var redirectUri = z18.string().min(1).max(2e3).refine((v) => {
|
|
|
4720
5294
|
return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
4721
5295
|
return false;
|
|
4722
5296
|
}, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
|
|
4723
|
-
var codeChallengeMethod =
|
|
5297
|
+
var codeChallengeMethod = z20.enum(["S256"]);
|
|
4724
5298
|
var MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
4725
|
-
var dynamicClientRegistrationRequest =
|
|
4726
|
-
redirect_uris:
|
|
5299
|
+
var dynamicClientRegistrationRequest = z20.object({
|
|
5300
|
+
redirect_uris: z20.array(redirectUri).min(1).max(MCP_DCR_MAX_REDIRECT_URIS).meta({
|
|
4727
5301
|
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
5302
|
}),
|
|
4729
|
-
client_name:
|
|
5303
|
+
client_name: z20.string().min(1).max(200).optional().meta({
|
|
4730
5304
|
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
5305
|
}),
|
|
4732
|
-
token_endpoint_auth_method:
|
|
5306
|
+
token_endpoint_auth_method: z20.enum(["none"]).optional().meta({
|
|
4733
5307
|
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
5308
|
}),
|
|
4735
|
-
grant_types:
|
|
5309
|
+
grant_types: z20.array(z20.enum(["authorization_code", "refresh_token"])).optional().meta({
|
|
4736
5310
|
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
5311
|
}),
|
|
4738
|
-
response_types:
|
|
5312
|
+
response_types: z20.array(z20.enum(["code"])).optional().meta({
|
|
4739
5313
|
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
5314
|
}),
|
|
4741
|
-
scope:
|
|
5315
|
+
scope: z20.string().max(500).optional().meta({
|
|
4742
5316
|
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
5317
|
})
|
|
4744
5318
|
}).meta({
|
|
4745
5319
|
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
5320
|
});
|
|
4747
|
-
var dynamicClientRegistrationResponse =
|
|
4748
|
-
client_id:
|
|
5321
|
+
var dynamicClientRegistrationResponse = z20.object({
|
|
5322
|
+
client_id: z20.string().min(1).max(200).meta({
|
|
4749
5323
|
description: "The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier."
|
|
4750
5324
|
}),
|
|
4751
|
-
client_name:
|
|
5325
|
+
client_name: z20.string().min(1).max(200).meta({
|
|
4752
5326
|
description: "The name the client registered under, echoed back. Chosen by the client and not vouched for by Fleetless."
|
|
4753
5327
|
}),
|
|
4754
|
-
redirect_uris:
|
|
5328
|
+
redirect_uris: z20.array(redirectUri).meta({
|
|
4755
5329
|
description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
|
|
4756
5330
|
}),
|
|
4757
|
-
grant_types:
|
|
5331
|
+
grant_types: z20.array(z20.string()).meta({
|
|
4758
5332
|
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
5333
|
}),
|
|
4760
|
-
response_types:
|
|
5334
|
+
response_types: z20.array(z20.string()).meta({
|
|
4761
5335
|
description: "The response types this client may ask for: `code`."
|
|
4762
5336
|
}),
|
|
4763
|
-
token_endpoint_auth_method:
|
|
5337
|
+
token_endpoint_auth_method: z20.literal("none").meta({
|
|
4764
5338
|
description: "`none` \u2014 this server registers public clients only, and PKCE rather than a secret is what protects the exchange."
|
|
4765
5339
|
}),
|
|
4766
|
-
client_id_issued_at:
|
|
5340
|
+
client_id_issued_at: z20.number().int().nonnegative().meta({
|
|
4767
5341
|
description: "When the registration was created, in seconds since the epoch, per RFC 7591."
|
|
4768
5342
|
}),
|
|
4769
|
-
client_secret_expires_at:
|
|
5343
|
+
client_secret_expires_at: z20.literal(0).meta({
|
|
4770
5344
|
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
5345
|
})
|
|
4772
5346
|
});
|
|
4773
|
-
var oauthCodeTokenRequest =
|
|
4774
|
-
grant_type:
|
|
5347
|
+
var oauthCodeTokenRequest = z20.object({
|
|
5348
|
+
grant_type: z20.literal("authorization_code").meta({
|
|
4775
5349
|
description: "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
|
|
4776
5350
|
}),
|
|
4777
|
-
code:
|
|
5351
|
+
code: z20.string().min(1).max(500).meta({
|
|
4778
5352
|
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
5353
|
}),
|
|
4780
5354
|
redirect_uri: redirectUri.meta({
|
|
4781
5355
|
description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
|
|
4782
5356
|
}),
|
|
4783
|
-
client_id:
|
|
5357
|
+
client_id: z20.string().min(1).max(200).meta({
|
|
4784
5358
|
description: "The client making the exchange, as registered."
|
|
4785
5359
|
}),
|
|
4786
|
-
code_verifier:
|
|
5360
|
+
code_verifier: z20.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
|
|
4787
5361
|
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
5362
|
}),
|
|
4789
|
-
resource:
|
|
5363
|
+
resource: z20.url().optional().meta({
|
|
4790
5364
|
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
5365
|
})
|
|
4792
5366
|
}).meta({
|
|
4793
5367
|
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
5368
|
});
|
|
4795
|
-
var oauthRefreshTokenRequest =
|
|
4796
|
-
grant_type:
|
|
5369
|
+
var oauthRefreshTokenRequest = z20.object({
|
|
5370
|
+
grant_type: z20.literal("refresh_token").meta({
|
|
4797
5371
|
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
5372
|
}),
|
|
4799
|
-
refresh_token:
|
|
5373
|
+
refresh_token: z20.string().min(1).max(500).meta({
|
|
4800
5374
|
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
5375
|
}),
|
|
4802
|
-
client_id:
|
|
5376
|
+
client_id: z20.string().min(1).max(200).meta({
|
|
4803
5377
|
description: "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
|
|
4804
5378
|
}),
|
|
4805
|
-
resource:
|
|
5379
|
+
resource: z20.url().optional().meta({
|
|
4806
5380
|
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
5381
|
})
|
|
4808
5382
|
}).meta({
|
|
4809
5383
|
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
5384
|
});
|
|
4811
|
-
var oauthTokenRequest =
|
|
5385
|
+
var oauthTokenRequest = z20.discriminatedUnion("grant_type", [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
|
|
4812
5386
|
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
5387
|
});
|
|
4814
|
-
var oauthTokenResponse =
|
|
4815
|
-
access_token:
|
|
5388
|
+
var oauthTokenResponse = z20.object({
|
|
5389
|
+
access_token: z20.string().min(1).meta({
|
|
4816
5390
|
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
5391
|
}),
|
|
4818
|
-
token_type:
|
|
5392
|
+
token_type: z20.literal("Bearer").meta({
|
|
4819
5393
|
description: "`Bearer`. RFC 6749 \xA75.1 makes the value case-insensitive for a client reading it; this is the spelling this server emits."
|
|
4820
5394
|
}),
|
|
4821
|
-
expires_in:
|
|
5395
|
+
expires_in: z20.number().int().positive().meta({
|
|
4822
5396
|
description: "How long the access token is valid, in **seconds**, per RFC 6749 \xA75.1. Not a timestamp, and not milliseconds."
|
|
4823
5397
|
}),
|
|
4824
|
-
refresh_token:
|
|
5398
|
+
refresh_token: z20.string().min(1).optional().meta({
|
|
4825
5399
|
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
5400
|
}),
|
|
4827
|
-
scope:
|
|
5401
|
+
scope: z20.string().max(500).optional().meta({
|
|
4828
5402
|
description: "The scopes the issued token actually carries, space-separated."
|
|
4829
5403
|
})
|
|
4830
5404
|
});
|
|
4831
|
-
var authorizationServerMetadata =
|
|
4832
|
-
issuer:
|
|
5405
|
+
var authorizationServerMetadata = z20.object({
|
|
5406
|
+
issuer: z20.url().meta({
|
|
4833
5407
|
description: "The issuer identifier of this authorization server, per RFC 8414 \xA72. It is what a client checks a token's `iss` against."
|
|
4834
5408
|
}),
|
|
4835
|
-
authorization_endpoint:
|
|
5409
|
+
authorization_endpoint: z20.url().meta({
|
|
4836
5410
|
description: "Where a client sends the user to authorize."
|
|
4837
5411
|
}),
|
|
4838
|
-
token_endpoint:
|
|
5412
|
+
token_endpoint: z20.url().meta({
|
|
4839
5413
|
description: "The URL where a client exchanges an authorization code, or a refresh token, for tokens."
|
|
4840
5414
|
}),
|
|
4841
|
-
registration_endpoint:
|
|
5415
|
+
registration_endpoint: z20.url().optional().meta({
|
|
4842
5416
|
description: "The URL where a client may register itself, per RFC 7591. Absent when the app does not accept dynamic clients."
|
|
4843
5417
|
}),
|
|
4844
|
-
response_types_supported:
|
|
5418
|
+
response_types_supported: z20.array(z20.literal("code")).meta({
|
|
4845
5419
|
description: "The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1."
|
|
4846
5420
|
}),
|
|
4847
|
-
grant_types_supported:
|
|
5421
|
+
grant_types_supported: z20.array(z20.enum(["authorization_code", "refresh_token"])).meta({
|
|
4848
5422
|
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
5423
|
}),
|
|
4850
|
-
code_challenge_methods_supported:
|
|
5424
|
+
code_challenge_methods_supported: z20.array(codeChallengeMethod).meta({
|
|
4851
5425
|
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
5426
|
}),
|
|
4853
|
-
token_endpoint_auth_methods_supported:
|
|
5427
|
+
token_endpoint_auth_methods_supported: z20.array(z20.literal("none")).meta({
|
|
4854
5428
|
description: "How a client authenticates at the token endpoint: `none`, the public-client method, with PKCE protecting the exchange."
|
|
4855
5429
|
}),
|
|
4856
|
-
scopes_supported:
|
|
5430
|
+
scopes_supported: z20.array(z20.string()).optional().meta({
|
|
4857
5431
|
description: "The scopes this server knows about, where it publishes a list."
|
|
4858
5432
|
})
|
|
4859
5433
|
});
|
|
4860
|
-
var protectedResourceMetadata =
|
|
4861
|
-
resource:
|
|
5434
|
+
var protectedResourceMetadata = z20.object({
|
|
5435
|
+
resource: z20.url().meta({
|
|
4862
5436
|
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
5437
|
}),
|
|
4864
|
-
authorization_servers:
|
|
5438
|
+
authorization_servers: z20.array(z20.url()).min(1).meta({
|
|
4865
5439
|
description: "The authorization servers that may issue tokens for this resource. There is always at least one."
|
|
4866
5440
|
}),
|
|
4867
|
-
bearer_methods_supported:
|
|
5441
|
+
bearer_methods_supported: z20.array(z20.literal("header")).meta({
|
|
4868
5442
|
description: "How a token may be presented: in the `Authorization` header only, never in a query parameter or a form field."
|
|
4869
5443
|
}),
|
|
4870
|
-
scopes_supported:
|
|
5444
|
+
scopes_supported: z20.array(z20.string()).optional().meta({
|
|
4871
5445
|
description: "The scopes this resource understands, where it publishes a list."
|
|
4872
5446
|
})
|
|
4873
5447
|
});
|
|
4874
|
-
var oauthRedirectResponse =
|
|
4875
|
-
redirect_to:
|
|
5448
|
+
var oauthRedirectResponse = z20.object({
|
|
5449
|
+
redirect_to: z20.string().min(1).max(2e3)
|
|
4876
5450
|
});
|
|
4877
|
-
var oauthAuthorizeQuery =
|
|
4878
|
-
response_type:
|
|
5451
|
+
var oauthAuthorizeQuery = z20.object({
|
|
5452
|
+
response_type: z20.literal("code").meta({
|
|
4879
5453
|
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
5454
|
}),
|
|
4881
|
-
client_id:
|
|
5455
|
+
client_id: z20.string().min(1).meta({
|
|
4882
5456
|
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
5457
|
}),
|
|
4884
|
-
redirect_uri:
|
|
5458
|
+
redirect_uri: z20.string().min(1).meta({
|
|
4885
5459
|
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
5460
|
}),
|
|
4887
|
-
code_challenge:
|
|
5461
|
+
code_challenge: z20.string().min(1).meta({
|
|
4888
5462
|
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
5463
|
}),
|
|
4890
|
-
code_challenge_method:
|
|
5464
|
+
code_challenge_method: z20.literal("S256").meta({
|
|
4891
5465
|
description: "Only `S256`. `plain` is refused: a challenge equal to its verifier defends against nothing."
|
|
4892
5466
|
}),
|
|
4893
|
-
state:
|
|
5467
|
+
state: z20.string().optional().meta({
|
|
4894
5468
|
description: "Returned unchanged on the callback, and on the error redirect too, so a client can bind either answer to its own request."
|
|
4895
5469
|
}),
|
|
4896
|
-
resource:
|
|
5470
|
+
resource: z20.string().optional().meta({
|
|
4897
5471
|
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
5472
|
})
|
|
4899
5473
|
// **No `scope`, because this authorization server issues none.** The field
|
|
@@ -4906,7 +5480,7 @@ var oauthAuthorizeQuery = z18.object({
|
|
|
4906
5480
|
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
5481
|
});
|
|
4908
5482
|
|
|
4909
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
5483
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/routes.js
|
|
4910
5484
|
var MCP_APP = MCP_APP_PATHS(":appIdentifier");
|
|
4911
5485
|
var APP_IDENTIFIER = {
|
|
4912
5486
|
name: "appIdentifier",
|
|
@@ -4923,11 +5497,123 @@ var IN_HANDLER_ROUTES = [
|
|
|
4923
5497
|
// The one route whose bearer is **optional**: it answers the same document
|
|
4924
5498
|
// with or without one, and only `already_granted` moves.
|
|
4925
5499
|
"GET /api/client/mcp/interactions/:id",
|
|
5500
|
+
// Two-factor setup: during sign-in the challenge in the body is the
|
|
5501
|
+
// credential, from the app's account settings the app user's bearer is.
|
|
5502
|
+
// Either one, decided in the handler.
|
|
5503
|
+
"POST /api/client/two-factor/setup",
|
|
5504
|
+
"POST /api/client/two-factor/setup/confirm",
|
|
4926
5505
|
"GET /api/asset-links/missing",
|
|
4927
5506
|
"GET /api/asset-links/:token"
|
|
4928
5507
|
];
|
|
4929
5508
|
var DEVELOPER_GUARD = ["unauthorized", "token_expired", "token_revoked"];
|
|
4930
5509
|
var CLIENT_GUARD = ["unauthorized", "token_expired", "token_revoked", "forbidden"];
|
|
5510
|
+
function developerSignInRoutes(prefix) {
|
|
5511
|
+
const section = prefix === "/console/oauth" ? "developer-auth" : "mcp";
|
|
5512
|
+
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";
|
|
5513
|
+
const page = (path, summary, notes) => ({
|
|
5514
|
+
method: "GET",
|
|
5515
|
+
path: `${prefix}${path}`,
|
|
5516
|
+
section,
|
|
5517
|
+
summary,
|
|
5518
|
+
audience: "internal",
|
|
5519
|
+
auth: "none",
|
|
5520
|
+
rateLimited: false,
|
|
5521
|
+
ownerTier: false,
|
|
5522
|
+
status: 200,
|
|
5523
|
+
params: [{ name: "id", description: "The interaction id of this sign-in; the step before redirects the browser here." }],
|
|
5524
|
+
query: null,
|
|
5525
|
+
request: null,
|
|
5526
|
+
response: null,
|
|
5527
|
+
errors: [],
|
|
5528
|
+
transport: "http",
|
|
5529
|
+
notes
|
|
5530
|
+
});
|
|
5531
|
+
const step = (path, summary, response, errors, notes) => ({
|
|
5532
|
+
method: "POST",
|
|
5533
|
+
path: `${prefix}${path}`,
|
|
5534
|
+
section,
|
|
5535
|
+
summary,
|
|
5536
|
+
audience: "internal",
|
|
5537
|
+
auth: "none",
|
|
5538
|
+
rateLimited: true,
|
|
5539
|
+
ownerTier: false,
|
|
5540
|
+
status: 200,
|
|
5541
|
+
params: [],
|
|
5542
|
+
query: null,
|
|
5543
|
+
request: null,
|
|
5544
|
+
response,
|
|
5545
|
+
errors: ["rate_limited", ...errors],
|
|
5546
|
+
transport: "http",
|
|
5547
|
+
notes
|
|
5548
|
+
});
|
|
5549
|
+
return [
|
|
5550
|
+
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\`.`),
|
|
5551
|
+
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."),
|
|
5552
|
+
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."),
|
|
5553
|
+
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."),
|
|
5554
|
+
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\`.`),
|
|
5555
|
+
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."),
|
|
5556
|
+
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\`.`),
|
|
5557
|
+
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."),
|
|
5558
|
+
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."),
|
|
5559
|
+
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`."),
|
|
5560
|
+
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."),
|
|
5561
|
+
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`."),
|
|
5562
|
+
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\`.`)
|
|
5563
|
+
];
|
|
5564
|
+
}
|
|
5565
|
+
var HOSTED_TOKEN = {
|
|
5566
|
+
name: "token",
|
|
5567
|
+
description: "The opaque token from the mailed link; it is never sent as a query parameter."
|
|
5568
|
+
};
|
|
5569
|
+
var HOSTED_INTERACTION = {
|
|
5570
|
+
name: "interaction",
|
|
5571
|
+
description: "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
5572
|
+
};
|
|
5573
|
+
function hostedPage(method, path, summary, notes, params = []) {
|
|
5574
|
+
return {
|
|
5575
|
+
method,
|
|
5576
|
+
path: `/app/:appIdentifier${path}`,
|
|
5577
|
+
section: "client-auth",
|
|
5578
|
+
summary,
|
|
5579
|
+
audience: "internal",
|
|
5580
|
+
auth: "none",
|
|
5581
|
+
rateLimited: method === "POST",
|
|
5582
|
+
ownerTier: false,
|
|
5583
|
+
status: 200,
|
|
5584
|
+
params: [APP_IDENTIFIER, ...params],
|
|
5585
|
+
query: null,
|
|
5586
|
+
request: null,
|
|
5587
|
+
response: null,
|
|
5588
|
+
errors: method === "POST" ? ["rate_limited"] : [],
|
|
5589
|
+
transport: "http",
|
|
5590
|
+
notes
|
|
5591
|
+
};
|
|
5592
|
+
}
|
|
5593
|
+
var HOSTED_FORM = "Renders a form; only its `POST` spends the token, so a mail scanner opening the link changes nothing. ";
|
|
5594
|
+
var HOSTED_DEAD = "A spent, expired or unknown token renders the `410` page with the next step for its kind.";
|
|
5595
|
+
var HOSTED_POST = "HTML: the next page on success, the same page with the problem named on a refusal. ";
|
|
5596
|
+
var HOSTED_APP_ROUTES = [
|
|
5597
|
+
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."),
|
|
5598
|
+
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]),
|
|
5599
|
+
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]),
|
|
5600
|
+
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]),
|
|
5601
|
+
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]),
|
|
5602
|
+
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]),
|
|
5603
|
+
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."),
|
|
5604
|
+
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."),
|
|
5605
|
+
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`."),
|
|
5606
|
+
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]),
|
|
5607
|
+
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."),
|
|
5608
|
+
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]),
|
|
5609
|
+
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."),
|
|
5610
|
+
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."),
|
|
5611
|
+
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.'),
|
|
5612
|
+
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.'),
|
|
5613
|
+
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.'),
|
|
5614
|
+
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]),
|
|
5615
|
+
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.")
|
|
5616
|
+
];
|
|
4931
5617
|
var ROUTES = [
|
|
4932
5618
|
/* ------------------------------------------------------------- health */
|
|
4933
5619
|
{
|
|
@@ -4949,24 +5635,6 @@ var ROUTES = [
|
|
|
4949
5635
|
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
5636
|
},
|
|
4951
5637
|
/* ----------------------------------------------------- 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
5638
|
{
|
|
4971
5639
|
method: "POST",
|
|
4972
5640
|
path: "/api/auth/refresh",
|
|
@@ -4983,7 +5651,7 @@ var ROUTES = [
|
|
|
4983
5651
|
response: sessionTokens,
|
|
4984
5652
|
errors: ["rate_limited", "validation_error", "token_expired", "token_revoked"],
|
|
4985
5653
|
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
|
|
5654
|
+
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
5655
|
},
|
|
4988
5656
|
{
|
|
4989
5657
|
method: "POST",
|
|
@@ -5038,11 +5706,12 @@ var ROUTES = [
|
|
|
5038
5706
|
transport: "http",
|
|
5039
5707
|
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
5708
|
},
|
|
5709
|
+
/* ------------------------------------ a developer's own second factors */
|
|
5041
5710
|
{
|
|
5042
|
-
method: "
|
|
5043
|
-
path: "/api/auth/
|
|
5711
|
+
method: "GET",
|
|
5712
|
+
path: "/api/auth/two-factor",
|
|
5044
5713
|
section: "developer-auth",
|
|
5045
|
-
summary: "
|
|
5714
|
+
summary: "Answers the calling developer's passkeys, authenticator, recovery codes left and the org's policy.",
|
|
5046
5715
|
audience: "developer",
|
|
5047
5716
|
auth: "developer",
|
|
5048
5717
|
rateLimited: false,
|
|
@@ -5050,102 +5719,173 @@ var ROUTES = [
|
|
|
5050
5719
|
status: 200,
|
|
5051
5720
|
params: [],
|
|
5052
5721
|
query: null,
|
|
5053
|
-
request:
|
|
5054
|
-
response:
|
|
5055
|
-
errors: [...DEVELOPER_GUARD
|
|
5722
|
+
request: null,
|
|
5723
|
+
response: developerTwoFactor,
|
|
5724
|
+
errors: [...DEVELOPER_GUARD],
|
|
5056
5725
|
transport: "http",
|
|
5057
|
-
notes: "
|
|
5726
|
+
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
5727
|
},
|
|
5059
5728
|
{
|
|
5060
5729
|
method: "POST",
|
|
5061
|
-
path: "/api/auth/
|
|
5730
|
+
path: "/api/auth/passkeys/options",
|
|
5062
5731
|
section: "developer-auth",
|
|
5063
|
-
summary: "
|
|
5732
|
+
summary: "Answers the WebAuthn creation options for registering a passkey.",
|
|
5064
5733
|
audience: "developer",
|
|
5065
|
-
auth: "
|
|
5066
|
-
rateLimited:
|
|
5734
|
+
auth: "developer",
|
|
5735
|
+
rateLimited: false,
|
|
5067
5736
|
ownerTier: false,
|
|
5068
|
-
status:
|
|
5737
|
+
status: 200,
|
|
5069
5738
|
params: [],
|
|
5070
5739
|
query: null,
|
|
5071
|
-
request:
|
|
5072
|
-
response:
|
|
5073
|
-
errors: [
|
|
5740
|
+
request: null,
|
|
5741
|
+
response: webauthnOptionsResponse,
|
|
5742
|
+
errors: [...DEVELOPER_GUARD],
|
|
5074
5743
|
transport: "http",
|
|
5075
|
-
notes:
|
|
5744
|
+
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
5745
|
},
|
|
5077
|
-
/* ------------------------------------------- client auth (portal pages) */
|
|
5078
5746
|
{
|
|
5079
|
-
method: "
|
|
5080
|
-
path: "/
|
|
5081
|
-
section: "
|
|
5082
|
-
summary:
|
|
5083
|
-
audience: "
|
|
5084
|
-
auth: "
|
|
5747
|
+
method: "POST",
|
|
5748
|
+
path: "/api/auth/passkeys",
|
|
5749
|
+
section: "developer-auth",
|
|
5750
|
+
summary: "Registers a passkey from the browser's answer to the creation options.",
|
|
5751
|
+
audience: "developer",
|
|
5752
|
+
auth: "developer",
|
|
5085
5753
|
rateLimited: false,
|
|
5086
5754
|
ownerTier: false,
|
|
5087
|
-
status:
|
|
5755
|
+
status: 201,
|
|
5088
5756
|
params: [],
|
|
5089
5757
|
query: null,
|
|
5090
|
-
request:
|
|
5091
|
-
response:
|
|
5092
|
-
errors: [],
|
|
5758
|
+
request: createPasskeyRequest,
|
|
5759
|
+
response: createPasskeyResponse,
|
|
5760
|
+
errors: [...DEVELOPER_GUARD, "validation_error"],
|
|
5093
5761
|
transport: "http",
|
|
5094
|
-
notes:
|
|
5762
|
+
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
5763
|
},
|
|
5096
5764
|
{
|
|
5097
|
-
method: "
|
|
5098
|
-
path: "/
|
|
5099
|
-
section: "
|
|
5100
|
-
summary:
|
|
5101
|
-
audience: "
|
|
5102
|
-
auth: "
|
|
5765
|
+
method: "PATCH",
|
|
5766
|
+
path: "/api/auth/passkeys/:id",
|
|
5767
|
+
section: "developer-auth",
|
|
5768
|
+
summary: "Renames one of the caller's passkeys.",
|
|
5769
|
+
audience: "developer",
|
|
5770
|
+
auth: "developer",
|
|
5103
5771
|
rateLimited: false,
|
|
5104
5772
|
ownerTier: false,
|
|
5105
5773
|
status: 200,
|
|
5106
|
-
params: [{ name: "
|
|
5774
|
+
params: [{ name: "id", description: "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`." }],
|
|
5775
|
+
query: null,
|
|
5776
|
+
request: renamePasskeyRequest,
|
|
5777
|
+
response: developerPasskey,
|
|
5778
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5779
|
+
transport: "http"
|
|
5780
|
+
},
|
|
5781
|
+
{
|
|
5782
|
+
method: "DELETE",
|
|
5783
|
+
path: "/api/auth/passkeys/:id",
|
|
5784
|
+
section: "developer-auth",
|
|
5785
|
+
summary: "Removes one of the caller's passkeys.",
|
|
5786
|
+
audience: "developer",
|
|
5787
|
+
auth: "developer",
|
|
5788
|
+
rateLimited: false,
|
|
5789
|
+
ownerTier: false,
|
|
5790
|
+
status: 204,
|
|
5791
|
+
params: [{ name: "id", description: "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`." }],
|
|
5107
5792
|
query: null,
|
|
5108
5793
|
request: null,
|
|
5109
5794
|
response: null,
|
|
5110
|
-
errors: [],
|
|
5795
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict"],
|
|
5111
5796
|
transport: "http",
|
|
5112
|
-
notes:
|
|
5797
|
+
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`."
|
|
5113
5798
|
},
|
|
5114
5799
|
{
|
|
5115
|
-
method: "
|
|
5116
|
-
path: "/
|
|
5117
|
-
section: "
|
|
5118
|
-
summary: "
|
|
5119
|
-
audience: "
|
|
5120
|
-
auth: "
|
|
5800
|
+
method: "POST",
|
|
5801
|
+
path: "/api/auth/totp",
|
|
5802
|
+
section: "developer-auth",
|
|
5803
|
+
summary: "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
5804
|
+
audience: "developer",
|
|
5805
|
+
auth: "developer",
|
|
5121
5806
|
rateLimited: false,
|
|
5122
5807
|
ownerTier: false,
|
|
5123
5808
|
status: 200,
|
|
5124
5809
|
params: [],
|
|
5125
5810
|
query: null,
|
|
5126
5811
|
request: null,
|
|
5127
|
-
response:
|
|
5128
|
-
errors: [],
|
|
5812
|
+
response: twoFactorSetupResponse,
|
|
5813
|
+
errors: [...DEVELOPER_GUARD],
|
|
5129
5814
|
transport: "http",
|
|
5130
|
-
notes: "
|
|
5815
|
+
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."
|
|
5131
5816
|
},
|
|
5132
5817
|
{
|
|
5133
5818
|
method: "POST",
|
|
5134
|
-
path: "/api/auth/
|
|
5819
|
+
path: "/api/auth/totp/confirm",
|
|
5135
5820
|
section: "developer-auth",
|
|
5136
|
-
summary: "
|
|
5821
|
+
summary: "Confirms the pending authenticator with a code it shows now.",
|
|
5137
5822
|
audience: "developer",
|
|
5138
|
-
auth: "
|
|
5823
|
+
auth: "developer",
|
|
5139
5824
|
rateLimited: true,
|
|
5140
5825
|
ownerTier: false,
|
|
5141
|
-
status:
|
|
5826
|
+
status: 200,
|
|
5827
|
+
params: [],
|
|
5828
|
+
query: null,
|
|
5829
|
+
request: totpConfirmRequest,
|
|
5830
|
+
response: totpConfirmResponse,
|
|
5831
|
+
errors: [...DEVELOPER_GUARD, "rate_limited", "validation_error", "invalid_code", "token_spent"],
|
|
5832
|
+
transport: "http",
|
|
5833
|
+
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`."
|
|
5834
|
+
},
|
|
5835
|
+
{
|
|
5836
|
+
method: "DELETE",
|
|
5837
|
+
path: "/api/auth/totp",
|
|
5838
|
+
section: "developer-auth",
|
|
5839
|
+
summary: "Removes the caller's authenticator app.",
|
|
5840
|
+
audience: "developer",
|
|
5841
|
+
auth: "developer",
|
|
5842
|
+
rateLimited: false,
|
|
5843
|
+
ownerTier: false,
|
|
5844
|
+
status: 204,
|
|
5142
5845
|
params: [],
|
|
5143
5846
|
query: null,
|
|
5144
|
-
request:
|
|
5847
|
+
request: null,
|
|
5145
5848
|
response: null,
|
|
5146
|
-
errors: [
|
|
5849
|
+
errors: [...DEVELOPER_GUARD, "not_found", "target_state_conflict"],
|
|
5147
5850
|
transport: "http",
|
|
5148
|
-
notes: "
|
|
5851
|
+
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`."
|
|
5852
|
+
},
|
|
5853
|
+
{
|
|
5854
|
+
method: "POST",
|
|
5855
|
+
path: "/api/auth/recovery-codes",
|
|
5856
|
+
section: "developer-auth",
|
|
5857
|
+
summary: "Issues ten new recovery codes and voids the old ones.",
|
|
5858
|
+
audience: "developer",
|
|
5859
|
+
auth: "developer",
|
|
5860
|
+
rateLimited: false,
|
|
5861
|
+
ownerTier: false,
|
|
5862
|
+
status: 200,
|
|
5863
|
+
params: [],
|
|
5864
|
+
query: null,
|
|
5865
|
+
request: null,
|
|
5866
|
+
response: recoveryCodesResponse,
|
|
5867
|
+
errors: [...DEVELOPER_GUARD, "target_state_conflict"],
|
|
5868
|
+
transport: "http",
|
|
5869
|
+
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`."
|
|
5870
|
+
},
|
|
5871
|
+
/* ------------------------------------------- client auth (portal pages) */
|
|
5872
|
+
{
|
|
5873
|
+
method: "GET",
|
|
5874
|
+
path: "/favicon.svg",
|
|
5875
|
+
section: "client-auth",
|
|
5876
|
+
summary: "Serves the Fleetless icon for the auth portal's and the MCP welcome page's browser tab.",
|
|
5877
|
+
audience: "internal",
|
|
5878
|
+
auth: "none",
|
|
5879
|
+
rateLimited: false,
|
|
5880
|
+
ownerTier: false,
|
|
5881
|
+
status: 200,
|
|
5882
|
+
params: [],
|
|
5883
|
+
query: null,
|
|
5884
|
+
request: null,
|
|
5885
|
+
response: null,
|
|
5886
|
+
errors: [],
|
|
5887
|
+
transport: "http",
|
|
5888
|
+
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."
|
|
5149
5889
|
},
|
|
5150
5890
|
{
|
|
5151
5891
|
method: "POST",
|
|
@@ -5328,7 +6068,7 @@ var ROUTES = [
|
|
|
5328
6068
|
response: role,
|
|
5329
6069
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error"],
|
|
5330
6070
|
transport: "http",
|
|
5331
|
-
notes: 'The body is `{ "name": string }` \u2014 non-empty, trimmed, at most
|
|
6071
|
+
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
6072
|
},
|
|
5333
6073
|
{
|
|
5334
6074
|
method: "GET",
|
|
@@ -5410,6 +6150,48 @@ var ROUTES = [
|
|
|
5410
6150
|
transport: "http",
|
|
5411
6151
|
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
6152
|
},
|
|
6153
|
+
{
|
|
6154
|
+
method: "PATCH",
|
|
6155
|
+
path: "/api/apps/:id/roles/:roleId",
|
|
6156
|
+
section: "apps",
|
|
6157
|
+
summary: "Renames a role; its users keep it.",
|
|
6158
|
+
audience: "developer",
|
|
6159
|
+
auth: "developer",
|
|
6160
|
+
rateLimited: false,
|
|
6161
|
+
ownerTier: false,
|
|
6162
|
+
status: 200,
|
|
6163
|
+
params: [
|
|
6164
|
+
{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." },
|
|
6165
|
+
{ name: "roleId", description: "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`." }
|
|
6166
|
+
],
|
|
6167
|
+
query: null,
|
|
6168
|
+
request: roleRenameRequest,
|
|
6169
|
+
response: role,
|
|
6170
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error", "role_name_taken"],
|
|
6171
|
+
transport: "http",
|
|
6172
|
+
notes: "Names are unique per app, compared exactly as stored after trimming. Built-in roles can be renamed."
|
|
6173
|
+
},
|
|
6174
|
+
{
|
|
6175
|
+
method: "DELETE",
|
|
6176
|
+
path: "/api/apps/:id/roles/:roleId",
|
|
6177
|
+
section: "apps",
|
|
6178
|
+
summary: "Deletes a role, moving its users, pending invitations and default-role status to another role.",
|
|
6179
|
+
audience: "developer",
|
|
6180
|
+
auth: "developer",
|
|
6181
|
+
rateLimited: false,
|
|
6182
|
+
ownerTier: false,
|
|
6183
|
+
status: 204,
|
|
6184
|
+
params: [
|
|
6185
|
+
{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." },
|
|
6186
|
+
{ name: "roleId", description: "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`." }
|
|
6187
|
+
],
|
|
6188
|
+
query: roleDeleteQuery,
|
|
6189
|
+
request: null,
|
|
6190
|
+
response: null,
|
|
6191
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error", "role_in_use", "last_role"],
|
|
6192
|
+
transport: "http",
|
|
6193
|
+
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."
|
|
6194
|
+
},
|
|
5413
6195
|
{
|
|
5414
6196
|
method: "POST",
|
|
5415
6197
|
path: "/api/apps/:id/server-keys",
|
|
@@ -5593,9 +6375,27 @@ var ROUTES = [
|
|
|
5593
6375
|
query: null,
|
|
5594
6376
|
request: null,
|
|
5595
6377
|
response: mailOutcome,
|
|
5596
|
-
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict"],
|
|
6378
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict", "method_not_allowed"],
|
|
5597
6379
|
transport: "http",
|
|
5598
|
-
notes: "The support door beside `POST /api/client/password/reset
|
|
6380
|
+
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."
|
|
6381
|
+
},
|
|
6382
|
+
{
|
|
6383
|
+
method: "DELETE",
|
|
6384
|
+
path: "/api/apps/:id/users/:userId/two-factor",
|
|
6385
|
+
section: "apps",
|
|
6386
|
+
summary: "Removes an app user's authenticator and recovery codes and ends every session they hold.",
|
|
6387
|
+
audience: "developer",
|
|
6388
|
+
auth: "developer",
|
|
6389
|
+
rateLimited: false,
|
|
6390
|
+
ownerTier: false,
|
|
6391
|
+
status: 204,
|
|
6392
|
+
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`." }],
|
|
6393
|
+
query: null,
|
|
6394
|
+
request: null,
|
|
6395
|
+
response: null,
|
|
6396
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
6397
|
+
transport: "http",
|
|
6398
|
+
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
6399
|
},
|
|
5600
6400
|
/* ---------------------------- the MCP clients one app user has connected */
|
|
5601
6401
|
{
|
|
@@ -5672,7 +6472,7 @@ var ROUTES = [
|
|
|
5672
6472
|
response: appInvitation,
|
|
5673
6473
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "email_taken", "target_state_conflict", "rate_limited"],
|
|
5674
6474
|
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
|
|
6475
|
+
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
6476
|
},
|
|
5677
6477
|
{
|
|
5678
6478
|
method: "POST",
|
|
@@ -5806,7 +6606,7 @@ var ROUTES = [
|
|
|
5806
6606
|
method: "GET",
|
|
5807
6607
|
path: "/api/apps/:id/auth-config",
|
|
5808
6608
|
section: "apps",
|
|
5809
|
-
summary: "Reads the app's auth settings:
|
|
6609
|
+
summary: "Reads the app's auth settings: sign-in methods, two-factor, registration, pages, the hosted look and the MCP switch.",
|
|
5810
6610
|
audience: "developer",
|
|
5811
6611
|
auth: "developer",
|
|
5812
6612
|
rateLimited: false,
|
|
@@ -5818,7 +6618,7 @@ var ROUTES = [
|
|
|
5818
6618
|
response: appAuthConfig,
|
|
5819
6619
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5820
6620
|
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."
|
|
6621
|
+
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
6622
|
},
|
|
5823
6623
|
{
|
|
5824
6624
|
method: "PUT",
|
|
@@ -5836,13 +6636,31 @@ var ROUTES = [
|
|
|
5836
6636
|
response: appAuthConfig,
|
|
5837
6637
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5838
6638
|
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
|
|
6639
|
+
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."
|
|
6640
|
+
},
|
|
6641
|
+
{
|
|
6642
|
+
method: "PUT",
|
|
6643
|
+
path: "/api/apps/:id/auth-config/sign-in",
|
|
6644
|
+
section: "apps",
|
|
6645
|
+
summary: "Replaces how the app's users sign in and whether they give a second factor.",
|
|
6646
|
+
audience: "developer",
|
|
6647
|
+
auth: "developer",
|
|
6648
|
+
rateLimited: false,
|
|
6649
|
+
ownerTier: false,
|
|
6650
|
+
status: 200,
|
|
6651
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6652
|
+
query: null,
|
|
6653
|
+
request: putAppAuthSignInRequest,
|
|
6654
|
+
response: appAuthConfig,
|
|
6655
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
6656
|
+
transport: "http",
|
|
6657
|
+
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
6658
|
},
|
|
5841
6659
|
{
|
|
5842
6660
|
method: "PUT",
|
|
5843
6661
|
path: "/api/apps/:id/auth-config/urls",
|
|
5844
6662
|
section: "apps",
|
|
5845
|
-
summary: "Replaces the
|
|
6663
|
+
summary: "Replaces the app's home page and the four pages Fleetless's mails and MCP sign-in point at.",
|
|
5846
6664
|
audience: "developer",
|
|
5847
6665
|
auth: "developer",
|
|
5848
6666
|
rateLimited: false,
|
|
@@ -5854,13 +6672,13 @@ var ROUTES = [
|
|
|
5854
6672
|
response: appAuthConfig,
|
|
5855
6673
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5856
6674
|
transport: "http",
|
|
5857
|
-
notes: "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `
|
|
6675
|
+
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
6676
|
},
|
|
5859
6677
|
{
|
|
5860
6678
|
method: "PUT",
|
|
5861
6679
|
path: "/api/apps/:id/auth-config/mcp",
|
|
5862
6680
|
section: "apps",
|
|
5863
|
-
summary: "
|
|
6681
|
+
summary: "Turns the app's MCP endpoint on or off.",
|
|
5864
6682
|
audience: "developer",
|
|
5865
6683
|
auth: "developer",
|
|
5866
6684
|
rateLimited: false,
|
|
@@ -5872,7 +6690,61 @@ var ROUTES = [
|
|
|
5872
6690
|
response: appAuthConfig,
|
|
5873
6691
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5874
6692
|
transport: "http",
|
|
5875
|
-
notes: "**A replace, not a merge, and `.strict()`**: `mcp_enabled`
|
|
6693
|
+
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."
|
|
6694
|
+
},
|
|
6695
|
+
{
|
|
6696
|
+
method: "PUT",
|
|
6697
|
+
path: "/api/apps/:id/auth-config/look",
|
|
6698
|
+
section: "apps",
|
|
6699
|
+
summary: "Replaces the hosted pages' accent colour.",
|
|
6700
|
+
audience: "developer",
|
|
6701
|
+
auth: "developer",
|
|
6702
|
+
rateLimited: false,
|
|
6703
|
+
ownerTier: false,
|
|
6704
|
+
status: 200,
|
|
6705
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6706
|
+
query: null,
|
|
6707
|
+
request: putAppAuthLookRequest,
|
|
6708
|
+
response: appAuthConfig,
|
|
6709
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
6710
|
+
transport: "http",
|
|
6711
|
+
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."
|
|
6712
|
+
},
|
|
6713
|
+
{
|
|
6714
|
+
method: "PUT",
|
|
6715
|
+
path: "/api/apps/:id/auth-config/logo",
|
|
6716
|
+
section: "apps",
|
|
6717
|
+
summary: "Stores the logo the hosted pages show above the app's name.",
|
|
6718
|
+
audience: "developer",
|
|
6719
|
+
auth: "developer",
|
|
6720
|
+
rateLimited: false,
|
|
6721
|
+
ownerTier: false,
|
|
6722
|
+
status: 200,
|
|
6723
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6724
|
+
query: null,
|
|
6725
|
+
request: null,
|
|
6726
|
+
response: appAuthConfig,
|
|
6727
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "unsupported_media_type"],
|
|
6728
|
+
transport: "http",
|
|
6729
|
+
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."
|
|
6730
|
+
},
|
|
6731
|
+
{
|
|
6732
|
+
method: "DELETE",
|
|
6733
|
+
path: "/api/apps/:id/auth-config/logo",
|
|
6734
|
+
section: "apps",
|
|
6735
|
+
summary: "Removes the logo from the hosted pages.",
|
|
6736
|
+
audience: "developer",
|
|
6737
|
+
auth: "developer",
|
|
6738
|
+
rateLimited: false,
|
|
6739
|
+
ownerTier: false,
|
|
6740
|
+
status: 200,
|
|
6741
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6742
|
+
query: null,
|
|
6743
|
+
request: null,
|
|
6744
|
+
response: appAuthConfig,
|
|
6745
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
6746
|
+
transport: "http",
|
|
6747
|
+
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
6748
|
},
|
|
5877
6749
|
{
|
|
5878
6750
|
method: "GET",
|
|
@@ -5890,7 +6762,7 @@ var ROUTES = [
|
|
|
5890
6762
|
response: appMailTemplateListResponse,
|
|
5891
6763
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5892
6764
|
transport: "http",
|
|
5893
|
-
notes: 'Answers `{ "templates": [appMailTemplate, \u2026] }` with **only the kinds that have a custom template** \u2014 at most
|
|
6765
|
+
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
6766
|
},
|
|
5895
6767
|
{
|
|
5896
6768
|
method: "GET",
|
|
@@ -5902,7 +6774,7 @@ var ROUTES = [
|
|
|
5902
6774
|
rateLimited: false,
|
|
5903
6775
|
ownerTier: false,
|
|
5904
6776
|
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
|
|
6777
|
+
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
6778
|
query: null,
|
|
5907
6779
|
request: null,
|
|
5908
6780
|
response: appMailTemplate,
|
|
@@ -5920,7 +6792,7 @@ var ROUTES = [
|
|
|
5920
6792
|
rateLimited: false,
|
|
5921
6793
|
ownerTier: false,
|
|
5922
6794
|
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
|
|
6795
|
+
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
6796
|
query: null,
|
|
5925
6797
|
request: putAppMailTemplateRequest,
|
|
5926
6798
|
response: appMailTemplate,
|
|
@@ -5938,7 +6810,7 @@ var ROUTES = [
|
|
|
5938
6810
|
rateLimited: false,
|
|
5939
6811
|
ownerTier: false,
|
|
5940
6812
|
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
|
|
6813
|
+
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
6814
|
query: null,
|
|
5943
6815
|
request: null,
|
|
5944
6816
|
response: null,
|
|
@@ -5956,7 +6828,7 @@ var ROUTES = [
|
|
|
5956
6828
|
rateLimited: false,
|
|
5957
6829
|
ownerTier: false,
|
|
5958
6830
|
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
|
|
6831
|
+
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
6832
|
query: null,
|
|
5961
6833
|
request: mailTemplatePreviewRequest,
|
|
5962
6834
|
response: mailTemplatePreviewResponse,
|
|
@@ -5974,7 +6846,7 @@ var ROUTES = [
|
|
|
5974
6846
|
rateLimited: false,
|
|
5975
6847
|
ownerTier: false,
|
|
5976
6848
|
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
|
|
6849
|
+
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
6850
|
query: null,
|
|
5979
6851
|
request: mailTemplatePreviewRequest,
|
|
5980
6852
|
response: mailOutcome,
|
|
@@ -6094,7 +6966,7 @@ var ROUTES = [
|
|
|
6094
6966
|
method: "POST",
|
|
6095
6967
|
path: "/api/org/invitations/accept",
|
|
6096
6968
|
section: "users",
|
|
6097
|
-
summary: "Spends an invitation token and creates the
|
|
6969
|
+
summary: "Spends an invitation token and creates the account it was addressed to.",
|
|
6098
6970
|
audience: "developer",
|
|
6099
6971
|
auth: "none",
|
|
6100
6972
|
rateLimited: true,
|
|
@@ -6106,7 +6978,7 @@ var ROUTES = [
|
|
|
6106
6978
|
response: null,
|
|
6107
6979
|
errors: ["rate_limited", "validation_error", "token_spent", "email_taken"],
|
|
6108
6980
|
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.'
|
|
6981
|
+
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
6982
|
},
|
|
6111
6983
|
{
|
|
6112
6984
|
method: "GET",
|
|
@@ -6124,7 +6996,7 @@ var ROUTES = [
|
|
|
6124
6996
|
response: null,
|
|
6125
6997
|
errors: [],
|
|
6126
6998
|
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
|
|
6999
|
+
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
7000
|
},
|
|
6129
7001
|
{
|
|
6130
7002
|
method: "PATCH",
|
|
@@ -6180,11 +7052,29 @@ var ROUTES = [
|
|
|
6180
7052
|
transport: "http",
|
|
6181
7053
|
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
7054
|
},
|
|
7055
|
+
{
|
|
7056
|
+
method: "DELETE",
|
|
7057
|
+
path: "/api/org/users/:id/two-factor",
|
|
7058
|
+
section: "users",
|
|
7059
|
+
summary: "Removes a team member's passkeys, authenticator and recovery codes and ends their sessions.",
|
|
7060
|
+
audience: "developer",
|
|
7061
|
+
auth: "developer",
|
|
7062
|
+
rateLimited: false,
|
|
7063
|
+
ownerTier: true,
|
|
7064
|
+
status: 204,
|
|
7065
|
+
params: [{ name: "id", description: "The Fleetless user's uuid, as listed by `GET /api/org/users`." }],
|
|
7066
|
+
query: null,
|
|
7067
|
+
request: null,
|
|
7068
|
+
response: null,
|
|
7069
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "invalid_uuid", "not_found", "target_state_conflict"],
|
|
7070
|
+
transport: "http",
|
|
7071
|
+
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."
|
|
7072
|
+
},
|
|
6183
7073
|
{
|
|
6184
7074
|
method: "PATCH",
|
|
6185
7075
|
path: "/api/org",
|
|
6186
7076
|
section: "org",
|
|
6187
|
-
summary: "Renames the org.",
|
|
7077
|
+
summary: "Renames the org, requires two-factor for its members, or both.",
|
|
6188
7078
|
audience: "developer",
|
|
6189
7079
|
auth: "developer",
|
|
6190
7080
|
rateLimited: false,
|
|
@@ -6196,7 +7086,7 @@ var ROUTES = [
|
|
|
6196
7086
|
response: patchOrgResponse,
|
|
6197
7087
|
errors: [...DEVELOPER_GUARD, "tier_required", "validation_error"],
|
|
6198
7088
|
transport: "http",
|
|
6199
|
-
notes: 'Answers `{ "org": org }`. Owner tier, and the gate runs before the body is looked at, so a malformed
|
|
7089
|
+
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
7090
|
},
|
|
6201
7091
|
/* --------------------------------------------------------------- mcp */
|
|
6202
7092
|
{
|
|
@@ -6269,7 +7159,7 @@ var ROUTES = [
|
|
|
6269
7159
|
response: null,
|
|
6270
7160
|
errors: [],
|
|
6271
7161
|
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
|
|
7162
|
+
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
7163
|
},
|
|
6274
7164
|
{
|
|
6275
7165
|
method: "GET",
|
|
@@ -6287,44 +7177,9 @@ var ROUTES = [
|
|
|
6287
7177
|
response: null,
|
|
6288
7178
|
errors: [],
|
|
6289
7179
|
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`.'
|
|
7180
|
+
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
7181
|
},
|
|
7182
|
+
...developerSignInRoutes("/mcp/oauth"),
|
|
6328
7183
|
{
|
|
6329
7184
|
method: "GET",
|
|
6330
7185
|
path: "/mcp/oauth/consent/:id",
|
|
@@ -6335,7 +7190,7 @@ var ROUTES = [
|
|
|
6335
7190
|
rateLimited: false,
|
|
6336
7191
|
ownerTier: false,
|
|
6337
7192
|
status: 200,
|
|
6338
|
-
params: [{ name: "id", description: "The interaction id from the sign-in; the
|
|
7193
|
+
params: [{ name: "id", description: "The interaction id from the sign-in; the last sign-in step redirects the browser here." }],
|
|
6339
7194
|
query: null,
|
|
6340
7195
|
request: null,
|
|
6341
7196
|
response: null,
|
|
@@ -6414,31 +7269,32 @@ var ROUTES = [
|
|
|
6414
7269
|
response: null,
|
|
6415
7270
|
errors: [],
|
|
6416
7271
|
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."
|
|
7272
|
+
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
7273
|
},
|
|
7274
|
+
...developerSignInRoutes("/console/oauth"),
|
|
6419
7275
|
{
|
|
6420
|
-
method: "
|
|
6421
|
-
path: "/console/oauth/
|
|
7276
|
+
method: "GET",
|
|
7277
|
+
path: "/console/oauth/signup/:id",
|
|
6422
7278
|
section: "developer-auth",
|
|
6423
|
-
summary: "
|
|
7279
|
+
summary: "Serves step one of console sign-up, the email card.",
|
|
6424
7280
|
audience: "internal",
|
|
6425
7281
|
auth: "none",
|
|
6426
|
-
rateLimited:
|
|
7282
|
+
rateLimited: false,
|
|
6427
7283
|
ownerTier: false,
|
|
6428
7284
|
status: 200,
|
|
6429
|
-
params: [],
|
|
7285
|
+
params: [{ name: "id", description: "The interaction id minted by `GET /console/oauth/authorize` with `?prompt=create`." }],
|
|
6430
7286
|
query: null,
|
|
6431
7287
|
request: null,
|
|
6432
7288
|
response: null,
|
|
6433
|
-
errors: [
|
|
7289
|
+
errors: [],
|
|
6434
7290
|
transport: "http",
|
|
6435
|
-
notes: '
|
|
7291
|
+
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
7292
|
},
|
|
6437
7293
|
{
|
|
6438
7294
|
method: "POST",
|
|
6439
|
-
path: "/console/oauth/
|
|
7295
|
+
path: "/console/oauth/signup",
|
|
6440
7296
|
section: "developer-auth",
|
|
6441
|
-
summary: "
|
|
7297
|
+
summary: "Takes the sign-up email, mails a code and hands back the code step.",
|
|
6442
7298
|
audience: "internal",
|
|
6443
7299
|
auth: "none",
|
|
6444
7300
|
rateLimited: true,
|
|
@@ -6447,34 +7303,16 @@ var ROUTES = [
|
|
|
6447
7303
|
params: [],
|
|
6448
7304
|
query: null,
|
|
6449
7305
|
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
7306
|
response: null,
|
|
6469
|
-
errors: [],
|
|
7307
|
+
errors: ["rate_limited", "token_spent", "signup_closed", "validation_error", "email_taken"],
|
|
6470
7308
|
transport: "http",
|
|
6471
|
-
notes: '
|
|
7309
|
+
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
7310
|
},
|
|
6473
7311
|
{
|
|
6474
7312
|
method: "POST",
|
|
6475
|
-
path: "/console/oauth/signup",
|
|
7313
|
+
path: "/console/oauth/signup/code",
|
|
6476
7314
|
section: "developer-auth",
|
|
6477
|
-
summary: "
|
|
7315
|
+
summary: "Checks the sign-up code and hands back the organization step.",
|
|
6478
7316
|
audience: "internal",
|
|
6479
7317
|
auth: "none",
|
|
6480
7318
|
rateLimited: true,
|
|
@@ -6484,9 +7322,9 @@ var ROUTES = [
|
|
|
6484
7322
|
query: null,
|
|
6485
7323
|
request: null,
|
|
6486
7324
|
response: null,
|
|
6487
|
-
errors: ["rate_limited", "token_spent", "signup_closed", "validation_error", "
|
|
7325
|
+
errors: ["rate_limited", "token_spent", "signup_closed", "wrong_browser", "validation_error", "invalid_code"],
|
|
6488
7326
|
transport: "http",
|
|
6489
|
-
notes: 'A
|
|
7327
|
+
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
7328
|
},
|
|
6491
7329
|
{
|
|
6492
7330
|
method: "POST",
|
|
@@ -6504,7 +7342,7 @@ var ROUTES = [
|
|
|
6504
7342
|
response: oauthRedirectResponse,
|
|
6505
7343
|
errors: ["rate_limited", "token_spent", "signup_closed", "wrong_browser", "validation_error", "email_taken"],
|
|
6506
7344
|
transport: "http",
|
|
6507
|
-
notes: "The
|
|
7345
|
+
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
7346
|
},
|
|
6509
7347
|
{
|
|
6510
7348
|
method: "POST",
|
|
@@ -6524,6 +7362,8 @@ var ROUTES = [
|
|
|
6524
7362
|
transport: "http",
|
|
6525
7363
|
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
7364
|
},
|
|
7365
|
+
/* --------------------------------- the Fleetless-hosted app pages */
|
|
7366
|
+
...HOSTED_APP_ROUTES,
|
|
6527
7367
|
/* ---------------------------------------------------- mcp (the endpoint) */
|
|
6528
7368
|
{
|
|
6529
7369
|
method: "GET",
|
|
@@ -6650,7 +7490,7 @@ var ROUTES = [
|
|
|
6650
7490
|
response: authorizationServerMetadata,
|
|
6651
7491
|
errors: ["not_found"],
|
|
6652
7492
|
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
|
|
7493
|
+
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
7494
|
},
|
|
6655
7495
|
{
|
|
6656
7496
|
method: "POST",
|
|
@@ -6674,7 +7514,7 @@ var ROUTES = [
|
|
|
6674
7514
|
method: "GET",
|
|
6675
7515
|
path: MCP_APP.authorize,
|
|
6676
7516
|
section: "mcp",
|
|
6677
|
-
summary: "Starts an MCP sign-in and redirects the browser to the app's own login page.",
|
|
7517
|
+
summary: "Starts an MCP sign-in and redirects the browser to the app's own login page, or to the hosted one.",
|
|
6678
7518
|
audience: "client",
|
|
6679
7519
|
auth: "none",
|
|
6680
7520
|
rateLimited: false,
|
|
@@ -6684,9 +7524,9 @@ var ROUTES = [
|
|
|
6684
7524
|
query: oauthAuthorizeQuery,
|
|
6685
7525
|
request: null,
|
|
6686
7526
|
response: null,
|
|
6687
|
-
errors: ["not_found"
|
|
7527
|
+
errors: ["not_found"],
|
|
6688
7528
|
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**
|
|
7529
|
+
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
7530
|
},
|
|
6691
7531
|
{
|
|
6692
7532
|
method: "POST",
|
|
@@ -6720,10 +7560,46 @@ var ROUTES = [
|
|
|
6720
7560
|
params: [],
|
|
6721
7561
|
query: null,
|
|
6722
7562
|
request: clientLoginRequest,
|
|
6723
|
-
response:
|
|
6724
|
-
errors: ["rate_limited", "validation_error", "invalid_credentials"],
|
|
7563
|
+
response: clientSignInResult,
|
|
7564
|
+
errors: ["rate_limited", "validation_error", "invalid_credentials", "method_not_allowed"],
|
|
7565
|
+
transport: "http",
|
|
7566
|
+
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.'
|
|
7567
|
+
},
|
|
7568
|
+
{
|
|
7569
|
+
method: "POST",
|
|
7570
|
+
path: "/api/client/login/code",
|
|
7571
|
+
section: "client-auth",
|
|
7572
|
+
summary: "Mails a six-digit sign-in code, and answers the same whether or not the address exists.",
|
|
7573
|
+
audience: "client",
|
|
7574
|
+
auth: "none",
|
|
7575
|
+
rateLimited: true,
|
|
7576
|
+
ownerTier: false,
|
|
7577
|
+
status: 202,
|
|
7578
|
+
params: [],
|
|
7579
|
+
query: null,
|
|
7580
|
+
request: clientLoginCodeRequest,
|
|
7581
|
+
response: null,
|
|
7582
|
+
errors: ["rate_limited", "validation_error", "not_found", "method_not_allowed"],
|
|
6725
7583
|
transport: "http",
|
|
6726
|
-
notes:
|
|
7584
|
+
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."
|
|
7585
|
+
},
|
|
7586
|
+
{
|
|
7587
|
+
method: "POST",
|
|
7588
|
+
path: "/api/client/login/code/verify",
|
|
7589
|
+
section: "client-auth",
|
|
7590
|
+
summary: "Spends a mailed sign-in code and answers a session or a two-factor challenge.",
|
|
7591
|
+
audience: "client",
|
|
7592
|
+
auth: "none",
|
|
7593
|
+
rateLimited: true,
|
|
7594
|
+
ownerTier: false,
|
|
7595
|
+
status: 200,
|
|
7596
|
+
params: [],
|
|
7597
|
+
query: null,
|
|
7598
|
+
request: clientLoginCodeVerifyRequest,
|
|
7599
|
+
response: clientSignInResult,
|
|
7600
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent", "method_not_allowed"],
|
|
7601
|
+
transport: "http",
|
|
7602
|
+
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
7603
|
},
|
|
6728
7604
|
{
|
|
6729
7605
|
method: "POST",
|
|
@@ -6741,7 +7617,7 @@ var ROUTES = [
|
|
|
6741
7617
|
response: null,
|
|
6742
7618
|
errors: ["rate_limited", "validation_error", "not_found", "registration_closed", "domain_not_allowed", "target_state_conflict", "quota_exceeded"],
|
|
6743
7619
|
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
|
|
7620
|
+
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
7621
|
},
|
|
6746
7622
|
{
|
|
6747
7623
|
method: "POST",
|
|
@@ -6756,10 +7632,10 @@ var ROUTES = [
|
|
|
6756
7632
|
params: [],
|
|
6757
7633
|
query: null,
|
|
6758
7634
|
request: clientVerifyEmailRequest,
|
|
6759
|
-
response:
|
|
7635
|
+
response: clientSignInResult,
|
|
6760
7636
|
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
6761
7637
|
transport: "http",
|
|
6762
|
-
notes: "**The answer is a session, not a `204
|
|
7638
|
+
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
7639
|
},
|
|
6764
7640
|
{
|
|
6765
7641
|
method: "POST",
|
|
@@ -6793,9 +7669,9 @@ var ROUTES = [
|
|
|
6793
7669
|
query: null,
|
|
6794
7670
|
request: clientPasswordResetRequest,
|
|
6795
7671
|
response: null,
|
|
6796
|
-
errors: ["rate_limited", "validation_error", "not_found"],
|
|
7672
|
+
errors: ["rate_limited", "validation_error", "not_found", "method_not_allowed"],
|
|
6797
7673
|
transport: "http",
|
|
6798
|
-
notes: "
|
|
7674
|
+
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
7675
|
},
|
|
6800
7676
|
{
|
|
6801
7677
|
method: "POST",
|
|
@@ -6810,10 +7686,10 @@ var ROUTES = [
|
|
|
6810
7686
|
params: [],
|
|
6811
7687
|
query: null,
|
|
6812
7688
|
request: clientPasswordResetConfirmRequest,
|
|
6813
|
-
response:
|
|
6814
|
-
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
7689
|
+
response: clientSignInResult,
|
|
7690
|
+
errors: ["rate_limited", "validation_error", "token_spent", "method_not_allowed"],
|
|
6815
7691
|
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."
|
|
7692
|
+
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
7693
|
},
|
|
6818
7694
|
{
|
|
6819
7695
|
method: "POST",
|
|
@@ -6828,10 +7704,10 @@ var ROUTES = [
|
|
|
6828
7704
|
params: [],
|
|
6829
7705
|
query: null,
|
|
6830
7706
|
request: clientAcceptInvitationRequest,
|
|
6831
|
-
response:
|
|
7707
|
+
response: clientSignInResult,
|
|
6832
7708
|
errors: ["rate_limited", "validation_error", "token_spent", "email_taken", "target_state_conflict", "quota_exceeded"],
|
|
6833
7709
|
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."
|
|
7710
|
+
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
7711
|
},
|
|
6836
7712
|
{
|
|
6837
7713
|
method: "POST",
|
|
@@ -6883,9 +7759,9 @@ var ROUTES = [
|
|
|
6883
7759
|
query: null,
|
|
6884
7760
|
request: passwordChangeRequest,
|
|
6885
7761
|
response: sessionTokens,
|
|
6886
|
-
errors: [...CLIENT_GUARD, "validation_error", "invalid_credentials", "target_state_conflict"],
|
|
7762
|
+
errors: [...CLIENT_GUARD, "validation_error", "invalid_credentials", "target_state_conflict", "method_not_allowed"],
|
|
6887
7763
|
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.'
|
|
7764
|
+
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
7765
|
},
|
|
6890
7766
|
{
|
|
6891
7767
|
method: "GET",
|
|
@@ -6905,6 +7781,80 @@ var ROUTES = [
|
|
|
6905
7781
|
transport: "http",
|
|
6906
7782
|
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
7783
|
},
|
|
7784
|
+
/* ------------------------------------------------ app-user two-factor */
|
|
7785
|
+
{
|
|
7786
|
+
method: "POST",
|
|
7787
|
+
path: "/api/client/two-factor/verify",
|
|
7788
|
+
section: "client-auth",
|
|
7789
|
+
summary: "Answers a two-factor challenge with an authenticator or recovery code, and answers the session.",
|
|
7790
|
+
audience: "client",
|
|
7791
|
+
auth: "none",
|
|
7792
|
+
rateLimited: true,
|
|
7793
|
+
ownerTier: false,
|
|
7794
|
+
status: 200,
|
|
7795
|
+
params: [],
|
|
7796
|
+
query: null,
|
|
7797
|
+
request: clientTwoFactorVerifyRequest,
|
|
7798
|
+
response: sessionTokens,
|
|
7799
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent"],
|
|
7800
|
+
transport: "http",
|
|
7801
|
+
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`."
|
|
7802
|
+
},
|
|
7803
|
+
{
|
|
7804
|
+
method: "POST",
|
|
7805
|
+
path: "/api/client/two-factor/setup",
|
|
7806
|
+
section: "client-auth",
|
|
7807
|
+
summary: "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
7808
|
+
audience: "client",
|
|
7809
|
+
auth: "in_handler",
|
|
7810
|
+
rateLimited: true,
|
|
7811
|
+
ownerTier: false,
|
|
7812
|
+
status: 200,
|
|
7813
|
+
params: [],
|
|
7814
|
+
query: null,
|
|
7815
|
+
request: clientTwoFactorSetupRequest,
|
|
7816
|
+
requestOptional: true,
|
|
7817
|
+
response: twoFactorSetupResponse,
|
|
7818
|
+
errors: ["rate_limited", "validation_error", "token_spent", "unauthorized", "target_state_conflict"],
|
|
7819
|
+
transport: "http",
|
|
7820
|
+
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."
|
|
7821
|
+
},
|
|
7822
|
+
{
|
|
7823
|
+
method: "POST",
|
|
7824
|
+
path: "/api/client/two-factor/setup/confirm",
|
|
7825
|
+
section: "client-auth",
|
|
7826
|
+
summary: "Confirms the new authenticator with a code and answers the recovery codes and a session.",
|
|
7827
|
+
audience: "client",
|
|
7828
|
+
auth: "in_handler",
|
|
7829
|
+
rateLimited: true,
|
|
7830
|
+
ownerTier: false,
|
|
7831
|
+
status: 200,
|
|
7832
|
+
params: [],
|
|
7833
|
+
query: null,
|
|
7834
|
+
request: clientTwoFactorSetupConfirmRequest,
|
|
7835
|
+
response: clientTwoFactorSetupConfirmResponse,
|
|
7836
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent", "unauthorized"],
|
|
7837
|
+
transport: "http",
|
|
7838
|
+
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`."
|
|
7839
|
+
},
|
|
7840
|
+
{
|
|
7841
|
+
method: "DELETE",
|
|
7842
|
+
path: "/api/client/two-factor",
|
|
7843
|
+
section: "client-auth",
|
|
7844
|
+
summary: "Turns the signed-in app user's authenticator off.",
|
|
7845
|
+
audience: "client",
|
|
7846
|
+
auth: "developer_or_client",
|
|
7847
|
+
rateLimited: true,
|
|
7848
|
+
ownerTier: false,
|
|
7849
|
+
status: 204,
|
|
7850
|
+
params: [],
|
|
7851
|
+
query: null,
|
|
7852
|
+
request: clientTwoFactorDisableRequest,
|
|
7853
|
+
response: null,
|
|
7854
|
+
errors: [...CLIENT_GUARD, "rate_limited", "validation_error", "invalid_code", "target_state_conflict"],
|
|
7855
|
+
transport: "http",
|
|
7856
|
+
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`."
|
|
7857
|
+
},
|
|
6908
7858
|
/* ---------------------------------------- app-user sign-in through an IdP */
|
|
6909
7859
|
{
|
|
6910
7860
|
method: "GET",
|
|
@@ -6958,7 +7908,7 @@ var ROUTES = [
|
|
|
6958
7908
|
response: null,
|
|
6959
7909
|
errors: ["rate_limited"],
|
|
6960
7910
|
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.
|
|
7911
|
+
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
7912
|
},
|
|
6963
7913
|
{
|
|
6964
7914
|
method: "POST",
|
|
@@ -8149,6 +9099,97 @@ var ROUTES = [
|
|
|
8149
9099
|
transport: "http",
|
|
8150
9100
|
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
9101
|
},
|
|
9102
|
+
{
|
|
9103
|
+
method: "POST",
|
|
9104
|
+
path: "/api/feedback",
|
|
9105
|
+
section: "org",
|
|
9106
|
+
summary: "Sends a message from a developer to the people who build Fleetless.",
|
|
9107
|
+
audience: "developer",
|
|
9108
|
+
auth: "developer",
|
|
9109
|
+
rateLimited: true,
|
|
9110
|
+
ownerTier: false,
|
|
9111
|
+
status: 202,
|
|
9112
|
+
params: [],
|
|
9113
|
+
query: null,
|
|
9114
|
+
request: feedbackRequest,
|
|
9115
|
+
response: feedbackResponse,
|
|
9116
|
+
errors: [...DEVELOPER_GUARD, "validation_error", "rate_limited"],
|
|
9117
|
+
transport: "http",
|
|
9118
|
+
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."
|
|
9119
|
+
},
|
|
9120
|
+
/* ----------------------------------------------------------- org (plan) */
|
|
9121
|
+
{
|
|
9122
|
+
method: "GET",
|
|
9123
|
+
path: "/api/org/plan",
|
|
9124
|
+
section: "org",
|
|
9125
|
+
summary: "Reads the org's plan: its limits, its usage against them, its add-ons and any change already queued.",
|
|
9126
|
+
audience: "developer",
|
|
9127
|
+
auth: "developer",
|
|
9128
|
+
rateLimited: false,
|
|
9129
|
+
ownerTier: false,
|
|
9130
|
+
status: 200,
|
|
9131
|
+
params: [],
|
|
9132
|
+
query: null,
|
|
9133
|
+
request: null,
|
|
9134
|
+
response: orgPlan,
|
|
9135
|
+
errors: [...DEVELOPER_GUARD],
|
|
9136
|
+
transport: "http",
|
|
9137
|
+
notes: "The one read the console's Plan & billing page, its usage and limit gauges, and every upgrade prompt and feature gate draw from \u2014 nothing else computes `limits` or `usage` on its own. `limits` is already the effective ceiling, the catalogue row raised by `addons` or replaced by an operator's override, so a consumer never recomputes it from the catalogue. `usage` is counted fresh on every call, never cached. `switch` is present only for an organization still on the beta that has not yet landed on a priced plan."
|
|
9138
|
+
},
|
|
9139
|
+
{
|
|
9140
|
+
method: "PUT",
|
|
9141
|
+
path: "/api/org/plan/change",
|
|
9142
|
+
section: "org",
|
|
9143
|
+
summary: "Moves the org's plan down \u2014 a lower plan or a cancellation to Basic \u2014 queuing the change rather than applying it at once.",
|
|
9144
|
+
audience: "developer",
|
|
9145
|
+
auth: "developer",
|
|
9146
|
+
rateLimited: false,
|
|
9147
|
+
ownerTier: true,
|
|
9148
|
+
status: 200,
|
|
9149
|
+
params: [],
|
|
9150
|
+
query: null,
|
|
9151
|
+
request: planChangeRequest,
|
|
9152
|
+
response: orgPlan,
|
|
9153
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "validation_error", "plan_limit", "target_state_conflict"],
|
|
9154
|
+
transport: "http",
|
|
9155
|
+
notes: "Owner tier, and **downward only**: this route moves the org to a lower plan or cancels it outright to Basic. It never moves the org up \u2014 until payment exists, an upgrade or an add-on is not this route's job at all, and is handled today as a Feedback request that Fleetless then applies through the admin route. `409 target_state_conflict` names `target_plan` with rule `not_lower` when the chosen plan is not below the org's current one; with rule `locked_basic_only` when the org is locked (`orgLock`) and the chosen plan is anything but Basic; and with rule `migration_basic_only` when the org is still on the beta, awaiting the switch to priced plans, and the chosen plan is anything but Basic \u2014 that choice is exactly what the org lands on at the switch. An owner is never named in `keep` and always stays, but still counts against the target plan's `seats`; `409 plan_limit` names `seats` when the owners alone already exceed it, and names whichever other limit `keep` still exceeds otherwise. `keep` is `null` when the org's current usage already fits the target plan outright and nothing is deleted; named, it lists exactly the robots, apps, app users and developers that stay. The choice is stored as `pending_change` and takes effect at `period_ends_at` \u2014 **everything of the chosen kind not named in `keep` is deleted at that instant, never before** \u2014 except a choice made while the org is locked, which takes effect at once, and a beta org's choice, which takes effect at the switch date instead. Anything created while the choice is pending is checked against the target plan too and, when it passes, is folded into `keep`, so exactly what the confirmation counted is what is actually deleted. A later `PUT` replaces a still-pending choice outright."
|
|
9156
|
+
},
|
|
9157
|
+
{
|
|
9158
|
+
method: "DELETE",
|
|
9159
|
+
path: "/api/org/plan/change",
|
|
9160
|
+
section: "org",
|
|
9161
|
+
summary: "Withdraws a plan change that was queued but has not taken effect yet.",
|
|
9162
|
+
audience: "developer",
|
|
9163
|
+
auth: "developer",
|
|
9164
|
+
rateLimited: false,
|
|
9165
|
+
ownerTier: true,
|
|
9166
|
+
status: 204,
|
|
9167
|
+
params: [],
|
|
9168
|
+
query: null,
|
|
9169
|
+
request: null,
|
|
9170
|
+
response: null,
|
|
9171
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "not_found"],
|
|
9172
|
+
transport: "http",
|
|
9173
|
+
notes: "Owner tier. `404 not_found` when the org has no `pending_change` to withdraw. The org stays on its current plan, unchanged, as if the choice had never been made; a developer who wants a different one sends a new `PUT`, which would have replaced this one outright anyway."
|
|
9174
|
+
},
|
|
9175
|
+
{
|
|
9176
|
+
method: "PATCH",
|
|
9177
|
+
path: "/api/admin/orgs/:id/plan",
|
|
9178
|
+
section: "org",
|
|
9179
|
+
summary: "Changes an org's plan, add-ons, limit overrides, currency or billing period as the operator.",
|
|
9180
|
+
audience: "internal",
|
|
9181
|
+
auth: "ops",
|
|
9182
|
+
rateLimited: true,
|
|
9183
|
+
ownerTier: false,
|
|
9184
|
+
status: 200,
|
|
9185
|
+
params: [{ name: "id", description: "The org's uuid, whose plan, add-ons or overrides the operator is changing." }],
|
|
9186
|
+
query: null,
|
|
9187
|
+
request: adminPlanChangeRequest,
|
|
9188
|
+
response: orgPlan,
|
|
9189
|
+
errors: ["unauthorized", "not_found", "validation_error", "plan_limit", "rate_limited"],
|
|
9190
|
+
transport: "http",
|
|
9191
|
+
notes: "Every public host answers `404 not_found` for every `/api/admin/*` path \u2014 this route is reachable only on the cloud's private address \u2014 and that same address answers `404` here too while `OPS_API_TOKEN` is not configured, so a door with no key behind it reads exactly like one nobody opened. An unknown `:id` is the same `404`. Applies at once, never queued as a `pending_change`, and only when the org's current usage fits the result: `409 plan_limit` when a lower plan, a lowered override or a removed add-on would leave the org over a limit \u2014 the operator never deletes an org's things, only the owner's own choice does. On success it also withdraws any `pending_change`, ends a beta org's wait for the switch and lifts a lock. Audited as `org.plan_changed` with the `fleetless` actor, never a developer's \u2014 the row names what an operator did, not who in the org asked for it."
|
|
9192
|
+
},
|
|
8152
9193
|
/* ------------------------------------------------- assets (robot upload) */
|
|
8153
9194
|
{
|
|
8154
9195
|
method: "POST",
|
|
@@ -8164,9 +9205,9 @@ var ROUTES = [
|
|
|
8164
9205
|
query: null,
|
|
8165
9206
|
request: null,
|
|
8166
9207
|
response: asset,
|
|
8167
|
-
errors: ["unauthorized", "rate_limited", "validation_error", "not_found", "quota_exceeded", "bad_request"],
|
|
9208
|
+
errors: ["unauthorized", "rate_limited", "validation_error", "not_found", "file_too_large", "plan_limit", "quota_exceeded", "bad_request"],
|
|
8168
9209
|
transport: "http",
|
|
8169
|
-
notes: "The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file \u2014 its kind, its name, its sync id and its announced size \u2014 rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. **
|
|
9210
|
+
notes: "The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file \u2014 its kind, its name, its sync id and its announced size \u2014 rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. **One file can be at most `ASSET_FILE_MAX_BYTES`**, on every plan and for every kind, the URDF included: an announced size over it answers `413 file_too_large` with `max_bytes` and `size_bytes` before a byte is buffered; without a truthful size, a body over the limit answers the same code with the bytes that arrived as `size_bytes`, or with `size_bytes: null` when it ran past the server's body limit and nobody counted the bytes. Retrying does not help. A file that fits but finds the store full answers `409 plan_limit` (or `409 quota_exceeded` for an organisation still on the beta) carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never refused for the store; only meshes and textures are charged against it."
|
|
8170
9211
|
},
|
|
8171
9212
|
/* ------------------------------------ realtime and bridge transports */
|
|
8172
9213
|
{
|
|
@@ -8291,13 +9332,19 @@ var ServerKeyCredentials = class {
|
|
|
8291
9332
|
function displayNameField(displayName) {
|
|
8292
9333
|
return displayName === void 0 ? {} : { display_name: displayName };
|
|
8293
9334
|
}
|
|
9335
|
+
function passwordField(password2) {
|
|
9336
|
+
return password2 === void 0 ? {} : { password: password2 };
|
|
9337
|
+
}
|
|
9338
|
+
function challengeField(challenge) {
|
|
9339
|
+
return challenge === void 0 ? {} : { challenge };
|
|
9340
|
+
}
|
|
8294
9341
|
function createPublicAuthCalls(http, appIdentifier2) {
|
|
8295
9342
|
return {
|
|
8296
9343
|
async register(input) {
|
|
8297
9344
|
const body = {
|
|
8298
9345
|
app_identifier: appIdentifier2,
|
|
8299
9346
|
email: input.email,
|
|
8300
|
-
|
|
9347
|
+
...passwordField(input.password),
|
|
8301
9348
|
...displayNameField(input.displayName)
|
|
8302
9349
|
};
|
|
8303
9350
|
await http.request("/api/client/register", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
@@ -8309,6 +9356,10 @@ function createPublicAuthCalls(http, appIdentifier2) {
|
|
|
8309
9356
|
async requestPasswordReset(email) {
|
|
8310
9357
|
const body = { app_identifier: appIdentifier2, email };
|
|
8311
9358
|
await http.request("/api/client/password/reset", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
9359
|
+
},
|
|
9360
|
+
async requestLoginCode(email) {
|
|
9361
|
+
const body = { app_identifier: appIdentifier2, email };
|
|
9362
|
+
await http.request("/api/client/login/code", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
8312
9363
|
}
|
|
8313
9364
|
};
|
|
8314
9365
|
}
|
|
@@ -8316,6 +9367,11 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8316
9367
|
async function storeSession(tokens) {
|
|
8317
9368
|
await tokenStore.save(tokens);
|
|
8318
9369
|
}
|
|
9370
|
+
async function completeSignIn(result) {
|
|
9371
|
+
if ("status" in result) return { status: result.status, challenge: result.challenge };
|
|
9372
|
+
await storeSession(result);
|
|
9373
|
+
return { status: "signed_in" };
|
|
9374
|
+
}
|
|
8319
9375
|
async function identity() {
|
|
8320
9376
|
return http.request("/api/client/me", {});
|
|
8321
9377
|
}
|
|
@@ -8323,13 +9379,18 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8323
9379
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8324
9380
|
async verifyEmail(token) {
|
|
8325
9381
|
const body = { token };
|
|
8326
|
-
const
|
|
8327
|
-
|
|
9382
|
+
const result = await http.request("/api/client/verify-email", { method: "POST", skipAuth: true, body });
|
|
9383
|
+
return completeSignIn(result);
|
|
8328
9384
|
},
|
|
8329
9385
|
async login(email, password2) {
|
|
8330
9386
|
const body = { app_identifier: appIdentifier2, email, password: password2 };
|
|
8331
|
-
const
|
|
8332
|
-
|
|
9387
|
+
const result = await http.request("/api/client/login", { method: "POST", skipAuth: true, body });
|
|
9388
|
+
return completeSignIn(result);
|
|
9389
|
+
},
|
|
9390
|
+
async verifyLoginCode(email, code) {
|
|
9391
|
+
const body = { app_identifier: appIdentifier2, email, code };
|
|
9392
|
+
const result = await http.request("/api/client/login/code/verify", { method: "POST", skipAuth: true, body });
|
|
9393
|
+
return completeSignIn(result);
|
|
8333
9394
|
},
|
|
8334
9395
|
async logout() {
|
|
8335
9396
|
const session = await tokenStore.load();
|
|
@@ -8343,6 +9404,13 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8343
9404
|
await tokenStore.save(null);
|
|
8344
9405
|
},
|
|
8345
9406
|
async me() {
|
|
9407
|
+
const session = await tokenStore.load();
|
|
9408
|
+
if (!session) {
|
|
9409
|
+
throw new FleetlessError(
|
|
9410
|
+
"no_session",
|
|
9411
|
+
"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."
|
|
9412
|
+
);
|
|
9413
|
+
}
|
|
8346
9414
|
return identity();
|
|
8347
9415
|
},
|
|
8348
9416
|
async changePassword(currentPassword, newPassword) {
|
|
@@ -8352,23 +9420,61 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8352
9420
|
},
|
|
8353
9421
|
async confirmPasswordReset(token, newPassword) {
|
|
8354
9422
|
const body = { token, new_password: newPassword };
|
|
8355
|
-
const
|
|
8356
|
-
|
|
9423
|
+
const result = await http.request("/api/client/password/reset/confirm", { method: "POST", skipAuth: true, body });
|
|
9424
|
+
return completeSignIn(result);
|
|
8357
9425
|
},
|
|
8358
9426
|
async acceptInvitation(input) {
|
|
8359
9427
|
const body = {
|
|
8360
9428
|
token: input.token,
|
|
8361
|
-
|
|
9429
|
+
...passwordField(input.password),
|
|
8362
9430
|
...displayNameField(input.displayName)
|
|
8363
9431
|
};
|
|
8364
|
-
const
|
|
9432
|
+
const result = await http.request("/api/client/invitations/accept", { method: "POST", skipAuth: true, body });
|
|
9433
|
+
return completeSignIn(result);
|
|
9434
|
+
},
|
|
9435
|
+
async verifyTwoFactor(input) {
|
|
9436
|
+
const body = {
|
|
9437
|
+
challenge: input.challenge,
|
|
9438
|
+
...input.code !== void 0 ? { code: input.code } : {},
|
|
9439
|
+
...input.recoveryCode !== void 0 ? { recovery_code: input.recoveryCode } : {}
|
|
9440
|
+
};
|
|
9441
|
+
const tokens = await http.request("/api/client/two-factor/verify", { method: "POST", skipAuth: true, body });
|
|
8365
9442
|
await storeSession(tokens);
|
|
8366
9443
|
},
|
|
9444
|
+
async beginTwoFactorSetup(input) {
|
|
9445
|
+
const challenge = input?.challenge;
|
|
9446
|
+
const body = challengeField(challenge);
|
|
9447
|
+
const response = await http.request("/api/client/two-factor/setup", {
|
|
9448
|
+
method: "POST",
|
|
9449
|
+
skipAuth: challenge !== void 0,
|
|
9450
|
+
body
|
|
9451
|
+
});
|
|
9452
|
+
return { secret: response.secret, otpauthUrl: response.otpauth_url };
|
|
9453
|
+
},
|
|
9454
|
+
async confirmTwoFactorSetup(input) {
|
|
9455
|
+
const body = { code: input.code, ...challengeField(input.challenge) };
|
|
9456
|
+
const response = await http.request("/api/client/two-factor/setup/confirm", {
|
|
9457
|
+
method: "POST",
|
|
9458
|
+
skipAuth: input.challenge !== void 0,
|
|
9459
|
+
body
|
|
9460
|
+
});
|
|
9461
|
+
await storeSession(response.session);
|
|
9462
|
+
return { recoveryCodes: response.recovery_codes };
|
|
9463
|
+
},
|
|
9464
|
+
async disableTwoFactor(code) {
|
|
9465
|
+
const body = { code };
|
|
9466
|
+
await http.request("/api/client/two-factor", { method: "DELETE", expectEmptyBody: true, body });
|
|
9467
|
+
},
|
|
8367
9468
|
async listProviders() {
|
|
8368
9469
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
8369
9470
|
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
8370
9471
|
return response.providers;
|
|
8371
9472
|
},
|
|
9473
|
+
async signInMethods() {
|
|
9474
|
+
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
9475
|
+
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
9476
|
+
return { password: response.sign_in_methods.password, emailCode: response.sign_in_methods.email_code };
|
|
9477
|
+
},
|
|
8372
9478
|
async beginOidcLogin(input) {
|
|
8373
9479
|
const state = generateState();
|
|
8374
9480
|
const codeVerifier = generateCodeVerifier();
|
|
@@ -8452,11 +9558,11 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8452
9558
|
const subject = option === "serverKey" ? "a server key" : "a supplied credential";
|
|
8453
9559
|
const holder = option === "serverKey" ? "a server-key client" : "a client with a supplied credential";
|
|
8454
9560
|
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.
|
|
9561
|
+
// **`register`, `resendVerification`, `requestPasswordReset` and
|
|
9562
|
+
// `requestLoginCode` are allowed here** — see `createPublicAuthCalls`.
|
|
9563
|
+
// They are public routes that name their own subject and answer
|
|
9564
|
+
// nothing, so a server-rendered sign-up or forgot-password (or
|
|
9565
|
+
// sign-in-by-code) page can use the one client its backend already has.
|
|
8460
9566
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8461
9567
|
async verifyEmail() {
|
|
8462
9568
|
serverKeyRefusal("verifyEmail", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
@@ -8464,6 +9570,9 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8464
9570
|
async login() {
|
|
8465
9571
|
serverKeyRefusal("login", `${subject} IS the credential; there is nothing to exchange`);
|
|
8466
9572
|
},
|
|
9573
|
+
async verifyLoginCode() {
|
|
9574
|
+
serverKeyRefusal("verifyLoginCode", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
9575
|
+
},
|
|
8467
9576
|
async logout() {
|
|
8468
9577
|
serverKeyRefusal("logout", `${subject} holds no session to end`);
|
|
8469
9578
|
},
|
|
@@ -8479,11 +9588,31 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8479
9588
|
async acceptInvitation() {
|
|
8480
9589
|
serverKeyRefusal("acceptInvitation", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
8481
9590
|
},
|
|
9591
|
+
async verifyTwoFactor() {
|
|
9592
|
+
serverKeyRefusal("verifyTwoFactor", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
9593
|
+
},
|
|
9594
|
+
async beginTwoFactorSetup() {
|
|
9595
|
+
serverKeyRefusal("beginTwoFactorSetup", `a two-factor setup is a person's own account's, and ${subject} is not a person's session`);
|
|
9596
|
+
},
|
|
9597
|
+
async confirmTwoFactorSetup() {
|
|
9598
|
+
serverKeyRefusal(
|
|
9599
|
+
"confirmTwoFactorSetup",
|
|
9600
|
+
`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`
|
|
9601
|
+
);
|
|
9602
|
+
},
|
|
9603
|
+
async disableTwoFactor() {
|
|
9604
|
+
serverKeyRefusal("disableTwoFactor", `turning an authenticator off is a person's own decision, and ${subject} is not a person`);
|
|
9605
|
+
},
|
|
8482
9606
|
async listProviders() {
|
|
8483
9607
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
8484
9608
|
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
8485
9609
|
return response.providers;
|
|
8486
9610
|
},
|
|
9611
|
+
async signInMethods() {
|
|
9612
|
+
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
9613
|
+
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
9614
|
+
return { password: response.sign_in_methods.password, emailCode: response.sign_in_methods.email_code };
|
|
9615
|
+
},
|
|
8487
9616
|
async beginOidcLogin() {
|
|
8488
9617
|
serverKeyRefusal("beginOidcLogin", "a federated sign-in is inherently an app user's browser flow");
|
|
8489
9618
|
},
|