@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.cjs
CHANGED
|
@@ -275,7 +275,7 @@ function isRenderKind(kind) {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
-
async function forEachWithConcurrency(items,
|
|
278
|
+
async function forEachWithConcurrency(items, limit2, fn) {
|
|
279
279
|
let next = 0;
|
|
280
280
|
let failed = false;
|
|
281
281
|
let firstError;
|
|
@@ -293,7 +293,7 @@ async function forEachWithConcurrency(items, limit, fn) {
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
-
await Promise.all(Array.from({ length: Math.min(
|
|
296
|
+
await Promise.all(Array.from({ length: Math.min(limit2, items.length) }, () => worker()));
|
|
297
297
|
if (failed) throw firstError;
|
|
298
298
|
}
|
|
299
299
|
function createAssetsApi(http) {
|
|
@@ -442,7 +442,7 @@ function createAssetsApi(http) {
|
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
445
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/common.js
|
|
446
446
|
var import_zod = require("zod");
|
|
447
447
|
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.";
|
|
448
448
|
var slug = import_zod.z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
|
|
@@ -473,7 +473,7 @@ var applyError = import_zod.z.object({
|
|
|
473
473
|
details: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
474
474
|
});
|
|
475
475
|
|
|
476
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
476
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/mcp.js
|
|
477
477
|
var import_zod2 = require("zod");
|
|
478
478
|
function mcpAppEndpointPath(appIdentifier2) {
|
|
479
479
|
return `/mcp/${appIdentifier2}`;
|
|
@@ -522,10 +522,10 @@ var mcpRolePreviewResponse = import_zod2.z.object({
|
|
|
522
522
|
});
|
|
523
523
|
var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
|
|
524
524
|
|
|
525
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
525
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/protocol.js
|
|
526
526
|
var import_zod8 = require("zod");
|
|
527
527
|
|
|
528
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
528
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/assets.js
|
|
529
529
|
var import_zod3 = require("zod");
|
|
530
530
|
var assetKind = import_zod3.z.enum(["urdf", "mesh", "texture"]);
|
|
531
531
|
var asset = import_zod3.z.object({
|
|
@@ -845,10 +845,10 @@ var assetSyncBusyDetails = import_zod3.z.object({
|
|
|
845
845
|
started_at_ms: import_zod3.z.number().int().nonnegative()
|
|
846
846
|
});
|
|
847
847
|
|
|
848
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
848
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/config.js
|
|
849
849
|
var import_zod5 = require("zod");
|
|
850
850
|
|
|
851
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
851
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/alerts.js
|
|
852
852
|
var import_zod4 = require("zod");
|
|
853
853
|
var alertRowCondition = import_zod4.z.discriminatedUnion("kind", [
|
|
854
854
|
import_zod4.z.strictObject({
|
|
@@ -918,7 +918,7 @@ var putDatapointDisplayRequest = import_zod4.z.object({
|
|
|
918
918
|
y_max: import_zod4.z.number().finite().nullable()
|
|
919
919
|
}).strict();
|
|
920
920
|
|
|
921
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
921
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/config.js
|
|
922
922
|
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:`.";
|
|
923
923
|
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:`.";
|
|
924
924
|
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.";
|
|
@@ -2010,7 +2010,7 @@ var configState = import_zod5.z.object({
|
|
|
2010
2010
|
applied_errors: import_zod5.z.array(applyError).nullable()
|
|
2011
2011
|
});
|
|
2012
2012
|
|
|
2013
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2013
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/introspection.js
|
|
2014
2014
|
var import_zod6 = require("zod");
|
|
2015
2015
|
var rosGraphEntry = import_zod6.z.object({
|
|
2016
2016
|
name: rosName,
|
|
@@ -2049,7 +2049,7 @@ var typeDefinition = import_zod6.z.discriminatedUnion("kind", [
|
|
|
2049
2049
|
})
|
|
2050
2050
|
]);
|
|
2051
2051
|
|
|
2052
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2052
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/jobs.js
|
|
2053
2053
|
var import_zod7 = require("zod");
|
|
2054
2054
|
var jobState = import_zod7.z.enum(["running", "unknown", "succeeded", "failed", "cancelled", "lost"]);
|
|
2055
2055
|
var reportedJobState = jobState.exclude(["unknown"]);
|
|
@@ -2164,6 +2164,20 @@ var jobActor = import_zod7.z.object({
|
|
|
2164
2164
|
*/
|
|
2165
2165
|
label: import_zod7.z.string().min(1).max(200).meta({
|
|
2166
2166
|
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."
|
|
2167
|
+
}),
|
|
2168
|
+
/**
|
|
2169
|
+
* The person's display name, snapshotted beside `label` for the same
|
|
2170
|
+
* reason. **Required and nullable**, so a consumer never has to tell
|
|
2171
|
+
* "absent" from "null": every run a 5.3.0 cloud answers carries it, and
|
|
2172
|
+
* `null` means there is no name to show — a server key, a person without
|
|
2173
|
+
* one, or a run recorded before the field existed.
|
|
2174
|
+
*
|
|
2175
|
+
* `jobActor` stays a plain object, not `.strict()`: a consumer still on an
|
|
2176
|
+
* older contracts version then parses a newer cloud's answer by stripping
|
|
2177
|
+
* the key instead of refusing the whole run.
|
|
2178
|
+
*/
|
|
2179
|
+
name: import_zod7.z.string().min(1).max(200).nullable().meta({
|
|
2180
|
+
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."
|
|
2167
2181
|
})
|
|
2168
2182
|
});
|
|
2169
2183
|
var jobRunKind = import_zod7.z.enum(["action", "service"]);
|
|
@@ -2272,7 +2286,7 @@ var jobRunSummary = import_zod7.z.object({
|
|
|
2272
2286
|
since_ms: import_zod7.z.number().int().nonnegative()
|
|
2273
2287
|
});
|
|
2274
2288
|
|
|
2275
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2289
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/protocol.js
|
|
2276
2290
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2277
2291
|
var MAX_PATIENCE_MS = 12e4;
|
|
2278
2292
|
var MIN_PATIENCE_MS = 1e3;
|
|
@@ -2405,7 +2419,8 @@ var cloudCancel = import_zod8.z.object({
|
|
|
2405
2419
|
type: import_zod8.z.literal("cancel"),
|
|
2406
2420
|
request_id: import_zod8.z.string().min(1).max(64),
|
|
2407
2421
|
slug,
|
|
2408
|
-
job_id: import_zod8.z.uuid().nullable()
|
|
2422
|
+
job_id: import_zod8.z.uuid().nullable(),
|
|
2423
|
+
own_only: import_zod8.z.boolean().optional()
|
|
2409
2424
|
});
|
|
2410
2425
|
var CANCEL_RETURN_CODES = { none: 0, rejected: 1, unknown_goal_id: 2, goal_terminated: 3 };
|
|
2411
2426
|
var cancelReturnCode = import_zod8.z.number().int().min(0).max(3);
|
|
@@ -2669,11 +2684,11 @@ var bridgeCameraState = import_zod8.z.object({
|
|
|
2669
2684
|
request_id: import_zod8.z.string().min(1).max(64).nullable()
|
|
2670
2685
|
});
|
|
2671
2686
|
|
|
2672
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2687
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/config-issues.js
|
|
2673
2688
|
var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
|
|
2674
2689
|
var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
|
|
2675
2690
|
|
|
2676
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2691
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/rest.js
|
|
2677
2692
|
var import_zod9 = require("zod");
|
|
2678
2693
|
var robot = import_zod9.z.object({
|
|
2679
2694
|
id: import_zod9.z.uuid().meta({
|
|
@@ -3438,13 +3453,13 @@ var slugUsageResponse = import_zod9.z.object({
|
|
|
3438
3453
|
alert_count: import_zod9.z.number().int().nonnegative()
|
|
3439
3454
|
});
|
|
3440
3455
|
|
|
3441
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3456
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/realtime.js
|
|
3442
3457
|
var import_zod14 = require("zod");
|
|
3443
3458
|
|
|
3444
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3459
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
3445
3460
|
var import_zod13 = require("zod");
|
|
3446
3461
|
|
|
3447
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3462
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/apps.js
|
|
3448
3463
|
var import_zod10 = require("zod");
|
|
3449
3464
|
var appIdentifier = slug;
|
|
3450
3465
|
var app = import_zod10.z.object({
|
|
@@ -3585,7 +3600,7 @@ var role = import_zod10.z.object({
|
|
|
3585
3600
|
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`."
|
|
3586
3601
|
}),
|
|
3587
3602
|
builtin: import_zod10.z.boolean().meta({
|
|
3588
|
-
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.
|
|
3603
|
+
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."
|
|
3589
3604
|
})
|
|
3590
3605
|
});
|
|
3591
3606
|
var roleListResponse = import_zod10.z.object({
|
|
@@ -3593,6 +3608,27 @@ var roleListResponse = import_zod10.z.object({
|
|
|
3593
3608
|
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."
|
|
3594
3609
|
})
|
|
3595
3610
|
});
|
|
3611
|
+
var roleRenameRequest = import_zod10.z.object({
|
|
3612
|
+
name: import_zod10.z.string().trim().min(1).max(60).meta({
|
|
3613
|
+
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."
|
|
3614
|
+
})
|
|
3615
|
+
}).strict();
|
|
3616
|
+
var roleDeleteQuery = import_zod10.z.object({
|
|
3617
|
+
move_to: import_zod10.z.uuid().optional().meta({
|
|
3618
|
+
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`."
|
|
3619
|
+
})
|
|
3620
|
+
}).strict();
|
|
3621
|
+
var roleInUseDetails = import_zod10.z.object({
|
|
3622
|
+
users: import_zod10.z.number().int().nonnegative().meta({
|
|
3623
|
+
description: "App users whose role this is."
|
|
3624
|
+
}),
|
|
3625
|
+
invitations: import_zod10.z.number().int().nonnegative().meta({
|
|
3626
|
+
description: "Pending invitations that would grant this role when accepted."
|
|
3627
|
+
}),
|
|
3628
|
+
is_default: import_zod10.z.boolean().meta({
|
|
3629
|
+
description: "`true` when this is the app's `default_role_id`; the default then moves with the users to `move_to`."
|
|
3630
|
+
})
|
|
3631
|
+
}).strict();
|
|
3596
3632
|
var rolePermissions = import_zod10.z.object({
|
|
3597
3633
|
role_id: import_zod10.z.uuid(),
|
|
3598
3634
|
/**
|
|
@@ -3646,10 +3682,10 @@ var rolePermissions = import_zod10.z.object({
|
|
|
3646
3682
|
})
|
|
3647
3683
|
});
|
|
3648
3684
|
|
|
3649
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3685
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/app-users.js
|
|
3650
3686
|
var import_zod12 = require("zod");
|
|
3651
3687
|
|
|
3652
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3688
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/identity.js
|
|
3653
3689
|
var import_zod11 = require("zod");
|
|
3654
3690
|
var password = import_zod11.z.string().min(12).max(256);
|
|
3655
3691
|
var USER_DISPLAY_NAME_MAX = 120;
|
|
@@ -3661,6 +3697,9 @@ var org = import_zod11.z.object({
|
|
|
3661
3697
|
name: import_zod11.z.string().min(1).max(120).meta({
|
|
3662
3698
|
description: "The organisation's display name. Free text, changed through `PATCH /api/org`."
|
|
3663
3699
|
}),
|
|
3700
|
+
require_two_factor: import_zod11.z.boolean().meta({
|
|
3701
|
+
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`."
|
|
3702
|
+
}),
|
|
3664
3703
|
created_at: import_zod11.z.iso.datetime().meta({
|
|
3665
3704
|
description: "When the organisation was created, as an ISO 8601 timestamp."
|
|
3666
3705
|
})
|
|
@@ -3686,6 +3725,12 @@ var fleetlessUser = import_zod11.z.object({
|
|
|
3686
3725
|
tier: orgAdminTier.meta({
|
|
3687
3726
|
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."
|
|
3688
3727
|
}),
|
|
3728
|
+
two_factor: import_zod11.z.object({
|
|
3729
|
+
passkeys: import_zod11.z.number().int().min(0).meta({ description: "How many passkeys the person has registered." }),
|
|
3730
|
+
authenticator: import_zod11.z.boolean().meta({ description: "Whether the person has a confirmed authenticator app." })
|
|
3731
|
+
}).meta({
|
|
3732
|
+
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`."
|
|
3733
|
+
}),
|
|
3689
3734
|
created_at: import_zod11.z.iso.datetime().meta({
|
|
3690
3735
|
description: "When the account was created, as an ISO 8601 timestamp."
|
|
3691
3736
|
})
|
|
@@ -3707,21 +3752,19 @@ var sessionTokens = import_zod11.z.object({
|
|
|
3707
3752
|
})
|
|
3708
3753
|
});
|
|
3709
3754
|
var refreshRequest = import_zod11.z.object({ refresh_token: import_zod11.z.string().min(1) });
|
|
3710
|
-
var
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3755
|
+
var loginCode = import_zod11.z.string().regex(/^\d{6}$/, "must be exactly six digits");
|
|
3756
|
+
var totpCode = loginCode;
|
|
3757
|
+
var recoveryCode = import_zod11.z.string().regex(/^[a-zA-Z2-7]{5}-[a-zA-Z2-7]{5}$/, "must be two groups of five characters, xxxxx-xxxxx");
|
|
3758
|
+
var recoveryCodesList = import_zod11.z.array(import_zod11.z.string().regex(/^[a-z2-7]{5}-[a-z2-7]{5}$/)).length(10);
|
|
3759
|
+
var twoFactorSetupResponse = import_zod11.z.object({
|
|
3760
|
+
secret: import_zod11.z.string().min(1).meta({
|
|
3761
|
+
description: "The shared secret, base32, for an authenticator app that cannot scan a QR code. Shown once; the cloud stores it encrypted."
|
|
3762
|
+
}),
|
|
3763
|
+
otpauth_url: import_zod11.z.string().startsWith("otpauth://totp/").meta({
|
|
3764
|
+
description: "The same secret as an `otpauth://totp/` URL, to render as a QR code. It carries the secret: never log it."
|
|
3765
|
+
})
|
|
3719
3766
|
});
|
|
3720
3767
|
var waitlistRequest = import_zod11.z.object({ email: import_zod11.z.email().max(254) });
|
|
3721
|
-
var developerLoginRequest = import_zod11.z.object({
|
|
3722
|
-
email: import_zod11.z.email(),
|
|
3723
|
-
password: import_zod11.z.string().min(1)
|
|
3724
|
-
});
|
|
3725
3768
|
var mailStatus = import_zod11.z.enum(["sent", "not_requested", "not_configured", "failed"]);
|
|
3726
3769
|
var createTeamInviteRequest = import_zod11.z.object({
|
|
3727
3770
|
email: import_zod11.z.email().meta({
|
|
@@ -3762,8 +3805,8 @@ var acceptTeamInviteRequest = import_zod11.z.object({
|
|
|
3762
3805
|
token: import_zod11.z.string().min(1).meta({
|
|
3763
3806
|
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."
|
|
3764
3807
|
}),
|
|
3765
|
-
|
|
3766
|
-
description: "
|
|
3808
|
+
display_name: import_zod11.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable().optional().meta({
|
|
3809
|
+
description: "An optional name, overriding whatever the invitation pre-filled. Absent keeps it."
|
|
3767
3810
|
})
|
|
3768
3811
|
}).strict();
|
|
3769
3812
|
var patchFleetlessUserRequest = import_zod11.z.object({
|
|
@@ -3785,13 +3828,6 @@ var passwordChangeRequest = import_zod11.z.object({
|
|
|
3785
3828
|
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."
|
|
3786
3829
|
})
|
|
3787
3830
|
});
|
|
3788
|
-
var passwordResetRequest = import_zod11.z.object({
|
|
3789
|
-
email: import_zod11.z.email()
|
|
3790
|
-
});
|
|
3791
|
-
var passwordResetConfirm = import_zod11.z.object({
|
|
3792
|
-
token: import_zod11.z.string().min(1),
|
|
3793
|
-
new_password: password
|
|
3794
|
-
});
|
|
3795
3831
|
var idpIssuer = import_zod11.z.url().max(500).refine((v) => {
|
|
3796
3832
|
let url;
|
|
3797
3833
|
try {
|
|
@@ -3808,10 +3844,66 @@ var idpIssuer = import_zod11.z.url().max(500).refine((v) => {
|
|
|
3808
3844
|
return url.hostname.length > 0;
|
|
3809
3845
|
}, { message: "issuer must be an http(s) URL with no credentials, query or fragment" });
|
|
3810
3846
|
var authMeResponse = import_zod11.z.object({ org, user: fleetlessUser });
|
|
3811
|
-
var patchOrgRequest = import_zod11.z.object({
|
|
3847
|
+
var patchOrgRequest = import_zod11.z.object({
|
|
3848
|
+
name: import_zod11.z.string().min(1).max(120).optional().meta({
|
|
3849
|
+
description: "The organisation's new display name. Absent leaves it alone."
|
|
3850
|
+
}),
|
|
3851
|
+
require_two_factor: import_zod11.z.boolean().optional().meta({
|
|
3852
|
+
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."
|
|
3853
|
+
})
|
|
3854
|
+
}).strict().refine((b) => b.name !== void 0 || b.require_two_factor !== void 0, { message: "Send name, require_two_factor, or both." });
|
|
3812
3855
|
var patchAuthMeRequest = import_zod11.z.object({ display_name: import_zod11.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
|
|
3856
|
+
var webauthnJson = import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown());
|
|
3857
|
+
var webauthnOptionsResponse = import_zod11.z.object({
|
|
3858
|
+
options: webauthnJson.meta({
|
|
3859
|
+
description: "The `PublicKeyCredentialCreationOptionsJSON` or `PublicKeyCredentialRequestOptionsJSON` to pass to the browser. Its challenge is single-use and short-lived."
|
|
3860
|
+
})
|
|
3861
|
+
});
|
|
3862
|
+
var developerPasskey = import_zod11.z.object({
|
|
3863
|
+
id: import_zod11.z.uuid().meta({ description: "The passkey in the API, as renamed and removed through `/api/auth/passkeys/:id`." }),
|
|
3864
|
+
name: import_zod11.z.string().min(1).max(80).meta({ description: "What the person called it, such as the device it lives on." }),
|
|
3865
|
+
created_at: import_zod11.z.iso.datetime().meta({ description: "When it was registered." }),
|
|
3866
|
+
last_used_at: import_zod11.z.iso.datetime().nullable().meta({ description: "When it last signed the person in or confirmed a sign-in, or `null` if never." }),
|
|
3867
|
+
synced: import_zod11.z.boolean().nullable().meta({
|
|
3868
|
+
description: "Whether the authenticator reported the passkey as syncable across the person's devices (the backup-eligible flag), or `null` when it said nothing."
|
|
3869
|
+
})
|
|
3870
|
+
});
|
|
3871
|
+
var developerTwoFactor = import_zod11.z.object({
|
|
3872
|
+
passkeys: import_zod11.z.array(developerPasskey).meta({ description: "Every passkey the caller has registered, oldest first. Empty when none." }),
|
|
3873
|
+
authenticator: import_zod11.z.object({ created_at: import_zod11.z.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." }),
|
|
3874
|
+
recovery_codes_left: import_zod11.z.number().int().min(0).max(10).meta({
|
|
3875
|
+
description: "How many of the ten recovery codes are unspent. `0` while the caller has no second factor."
|
|
3876
|
+
}),
|
|
3877
|
+
required_by_org: import_zod11.z.boolean().meta({
|
|
3878
|
+
description: "Whether the organisation requires a second factor. While it does, the last one cannot be removed."
|
|
3879
|
+
})
|
|
3880
|
+
});
|
|
3881
|
+
var createPasskeyRequest = import_zod11.z.object({
|
|
3882
|
+
name: import_zod11.z.string().min(1).max(80).meta({ description: "What to call the passkey, such as the device it lives on." }),
|
|
3883
|
+
credential: webauthnJson.meta({ description: "The browser's `RegistrationResponseJSON` for the options `POST /api/auth/passkeys/options` answered." })
|
|
3884
|
+
}).strict();
|
|
3885
|
+
var createPasskeyResponse = import_zod11.z.object({
|
|
3886
|
+
passkey: developerPasskey.meta({ description: "The passkey as it is now stored." }),
|
|
3887
|
+
recovery_codes: recoveryCodesList.nullable().meta({
|
|
3888
|
+
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."
|
|
3889
|
+
})
|
|
3890
|
+
});
|
|
3891
|
+
var renamePasskeyRequest = import_zod11.z.object({
|
|
3892
|
+
name: import_zod11.z.string().min(1).max(80).meta({ description: "The new name." })
|
|
3893
|
+
}).strict();
|
|
3894
|
+
var totpConfirmRequest = import_zod11.z.object({
|
|
3895
|
+
code: totpCode.meta({ description: "A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it." })
|
|
3896
|
+
}).strict();
|
|
3897
|
+
var totpConfirmResponse = import_zod11.z.object({
|
|
3898
|
+
recovery_codes: recoveryCodesList.nullable().meta({
|
|
3899
|
+
description: "The ten recovery codes, shown once, when this is the account's first second factor; `null` otherwise."
|
|
3900
|
+
})
|
|
3901
|
+
});
|
|
3902
|
+
var recoveryCodesResponse = import_zod11.z.object({
|
|
3903
|
+
recovery_codes: recoveryCodesList.meta({ description: "The ten new recovery codes, lowercase, shown once. Every earlier code is void." })
|
|
3904
|
+
});
|
|
3813
3905
|
|
|
3814
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3906
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/app-users.js
|
|
3815
3907
|
var APP_USER_DISPLAY_NAME_MAX = 120;
|
|
3816
3908
|
var providerSlug = import_zod12.z.string().max(40).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "must be lowercase and hyphen-separated, starting with a letter");
|
|
3817
3909
|
var appUserStatus = import_zod12.z.enum(["pending_verification", "active", "blocked"]);
|
|
@@ -3849,6 +3941,19 @@ var appUser = import_zod12.z.object({
|
|
|
3849
3941
|
last_login_at: import_zod12.z.iso.datetime().nullable().meta({
|
|
3850
3942
|
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*."
|
|
3851
3943
|
}),
|
|
3944
|
+
two_factor: import_zod12.z.object({
|
|
3945
|
+
enabled: import_zod12.z.boolean().meta({
|
|
3946
|
+
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."
|
|
3947
|
+
}),
|
|
3948
|
+
enabled_at: import_zod12.z.iso.datetime().nullable().meta({
|
|
3949
|
+
description: "When the authenticator was confirmed, or `null` while `enabled` is `false`."
|
|
3950
|
+
}),
|
|
3951
|
+
recovery_codes_left: import_zod12.z.number().int().min(0).max(10).meta({
|
|
3952
|
+
description: "How many of the ten single-use recovery codes are still unspent. `0` while `enabled` is `false`."
|
|
3953
|
+
})
|
|
3954
|
+
}).meta({
|
|
3955
|
+
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`."
|
|
3956
|
+
}),
|
|
3852
3957
|
created_at: import_zod12.z.iso.datetime().meta({
|
|
3853
3958
|
description: "When the account was created, as an ISO 8601 timestamp."
|
|
3854
3959
|
})
|
|
@@ -3894,7 +3999,7 @@ var createAppInvitationRequest = import_zod12.z.object({
|
|
|
3894
3999
|
description: "An optional name to pre-fill the account with; the invitee can change it afterwards."
|
|
3895
4000
|
}),
|
|
3896
4001
|
send_mail: import_zod12.z.boolean().meta({
|
|
3897
|
-
description: "Whether Fleetless mails the invitation.
|
|
4002
|
+
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."
|
|
3898
4003
|
})
|
|
3899
4004
|
}).strict();
|
|
3900
4005
|
var appInvitation = import_zod12.z.object({
|
|
@@ -3903,11 +4008,11 @@ var appInvitation = import_zod12.z.object({
|
|
|
3903
4008
|
email: import_zod12.z.email().meta({ description: "The address the invitation was addressed to." }),
|
|
3904
4009
|
role_id: import_zod12.z.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." }),
|
|
3905
4010
|
expires_at: import_zod12.z.iso.datetime().meta({ description: "When the token stops working. Seven days from issue; an expired token answers exactly as an unknown one does." }),
|
|
3906
|
-
accept_url: import_zod12.z.url().max(500).
|
|
3907
|
-
description: "The link to give the invitee
|
|
4011
|
+
accept_url: import_zod12.z.url().max(500).meta({
|
|
4012
|
+
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."
|
|
3908
4013
|
}),
|
|
3909
4014
|
mail: mailStatus.meta({
|
|
3910
|
-
description: "What happened to the mail: `sent` means the SMTP server accepted it, not that it was delivered; `not_requested` means none was attempted
|
|
4015
|
+
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."
|
|
3911
4016
|
})
|
|
3912
4017
|
});
|
|
3913
4018
|
var pendingAppInvitation = appInvitation.omit({ accept_url: true, mail: true });
|
|
@@ -4000,6 +4105,31 @@ var allowedOrigin = import_zod12.z.string().max(200).refine((v) => {
|
|
|
4000
4105
|
}
|
|
4001
4106
|
}, { message: "must be a bare origin (scheme, host, port) over https, or over http on localhost" });
|
|
4002
4107
|
var emailDomain = import_zod12.z.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");
|
|
4108
|
+
var appSignInMethods = import_zod12.z.object({
|
|
4109
|
+
password: import_zod12.z.boolean().meta({
|
|
4110
|
+
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."
|
|
4111
|
+
}),
|
|
4112
|
+
email_code: import_zod12.z.boolean().meta({
|
|
4113
|
+
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."
|
|
4114
|
+
})
|
|
4115
|
+
}).strict().refine((m) => m.password || m.email_code, { message: "At least one sign-in method must be on.", path: ["password"] });
|
|
4116
|
+
var appTwoFactorPolicy = import_zod12.z.enum(["off", "optional", "required"]);
|
|
4117
|
+
var appHomeUrl = import_zod12.z.url().max(500).refine((v) => {
|
|
4118
|
+
try {
|
|
4119
|
+
const u = new URL(v);
|
|
4120
|
+
const hostOk = u.protocol === "https:" || u.protocol === "http:" && ["localhost", "127.0.0.1"].includes(u.hostname);
|
|
4121
|
+
return hostOk && !v.includes("{");
|
|
4122
|
+
} catch {
|
|
4123
|
+
return false;
|
|
4124
|
+
}
|
|
4125
|
+
}, { message: "An https URL (http only on localhost) without a placeholder." });
|
|
4126
|
+
var hostedAccent = import_zod12.z.string().regex(/^#[0-9a-f]{6}$/, "must be a lowercase hex colour, #rrggbb");
|
|
4127
|
+
var appHostedPages = import_zod12.z.object({
|
|
4128
|
+
invite_url: import_zod12.z.url().meta({ description: "The hosted invitation page, `<portal>/app/<identifier>/invite/{token}`." }),
|
|
4129
|
+
verify_url: import_zod12.z.url().meta({ description: "The hosted email-confirmation page, `<portal>/app/<identifier>/verify/{token}`." }),
|
|
4130
|
+
reset_url: import_zod12.z.url().meta({ description: "The hosted new-password page, `<portal>/app/<identifier>/reset/{token}`." }),
|
|
4131
|
+
mcp_login_url: import_zod12.z.url().meta({ description: "The hosted MCP sign-in, `<portal>/app/<identifier>/mcp/{interaction}`." })
|
|
4132
|
+
});
|
|
4003
4133
|
var appAuthConfig = import_zod12.z.object({
|
|
4004
4134
|
self_registration: import_zod12.z.boolean().meta({
|
|
4005
4135
|
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."
|
|
@@ -4014,16 +4144,34 @@ var appAuthConfig = import_zod12.z.object({
|
|
|
4014
4144
|
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."
|
|
4015
4145
|
}),
|
|
4016
4146
|
invite_url: appUrlTemplate("{token}").nullable().meta({
|
|
4017
|
-
description: "The page in the developer's app that accepts an invitation, with `{token}` where the token goes. `null`
|
|
4147
|
+
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."
|
|
4018
4148
|
}),
|
|
4019
4149
|
verify_url: appUrlTemplate("{token}").nullable().meta({
|
|
4020
|
-
description: "The page that confirms a new address, with `{token}` where the token goes.
|
|
4150
|
+
description: "The page that confirms a new address, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used."
|
|
4021
4151
|
}),
|
|
4022
4152
|
reset_url: appUrlTemplate("{token}").nullable().meta({
|
|
4023
|
-
description: "The page that takes a new password, with `{token}` where the token goes."
|
|
4153
|
+
description: "The page that takes a new password, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used."
|
|
4024
4154
|
}),
|
|
4025
4155
|
mcp_login_url: appUrlTemplate("{interaction}").nullable().meta({
|
|
4026
|
-
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."
|
|
4156
|
+
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."
|
|
4157
|
+
}),
|
|
4158
|
+
app_url: appHomeUrl.nullable().meta({
|
|
4159
|
+
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`."
|
|
4160
|
+
}),
|
|
4161
|
+
sign_in_methods: appSignInMethods.meta({
|
|
4162
|
+
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."
|
|
4163
|
+
}),
|
|
4164
|
+
two_factor: appTwoFactorPolicy.meta({
|
|
4165
|
+
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."
|
|
4166
|
+
}),
|
|
4167
|
+
hosted_logo_url: import_zod12.z.url().nullable().meta({
|
|
4168
|
+
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`."
|
|
4169
|
+
}),
|
|
4170
|
+
hosted_accent: hostedAccent.nullable().meta({
|
|
4171
|
+
description: "The accent colour of the hosted pages, `#rrggbb` in lowercase, or `null` for the neutral shell's own."
|
|
4172
|
+
}),
|
|
4173
|
+
hosted_pages: appHostedPages.meta({
|
|
4174
|
+
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."
|
|
4027
4175
|
}),
|
|
4028
4176
|
oidc_callback_url: import_zod12.z.url().meta({
|
|
4029
4177
|
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."
|
|
@@ -4031,18 +4179,20 @@ var appAuthConfig = import_zod12.z.object({
|
|
|
4031
4179
|
updated_at: import_zod12.z.iso.datetime().meta({ description: "When the configuration was last written, as an ISO 8601 timestamp." })
|
|
4032
4180
|
});
|
|
4033
4181
|
var putAppAuthRegistrationRequest = appAuthConfig.pick({ self_registration: true, allowed_domains: true, allowed_origins: true }).strict();
|
|
4034
|
-
var
|
|
4035
|
-
var
|
|
4036
|
-
var
|
|
4182
|
+
var putAppAuthSignInRequest = appAuthConfig.pick({ sign_in_methods: true, two_factor: true }).strict();
|
|
4183
|
+
var putAppAuthUrlsRequest = appAuthConfig.pick({ app_url: true, invite_url: true, verify_url: true, reset_url: true, mcp_login_url: true }).strict();
|
|
4184
|
+
var putAppAuthMcpRequest = appAuthConfig.pick({ mcp_enabled: true }).strict();
|
|
4185
|
+
var putAppAuthLookRequest = appAuthConfig.pick({ hosted_accent: true }).strict();
|
|
4186
|
+
var mailTemplateKind = import_zod12.z.enum(["invite", "verify", "reset", "login_code"]);
|
|
4037
4187
|
var appMailTemplate = import_zod12.z.object({
|
|
4038
|
-
kind: mailTemplateKind.meta({ description: "Which of the
|
|
4188
|
+
kind: mailTemplateKind.meta({ description: "Which of the four mails this template replaces." }),
|
|
4039
4189
|
subject: import_zod12.z.string().min(1).max(200).meta({ description: "The subject line, a Liquid template. Bounded because a subject is rendered into a header." }),
|
|
4040
4190
|
text: import_zod12.z.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." }),
|
|
4041
4191
|
html: import_zod12.z.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." }),
|
|
4042
4192
|
updated_at: import_zod12.z.iso.datetime().meta({ description: "When the template was last written, as an ISO 8601 timestamp." })
|
|
4043
4193
|
});
|
|
4044
4194
|
var appMailTemplateListResponse = import_zod12.z.object({
|
|
4045
|
-
templates: import_zod12.z.array(appMailTemplate).max(
|
|
4195
|
+
templates: import_zod12.z.array(appMailTemplate).max(4).meta({
|
|
4046
4196
|
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."
|
|
4047
4197
|
})
|
|
4048
4198
|
});
|
|
@@ -4064,7 +4214,7 @@ var mailOutcome = import_zod12.z.object({
|
|
|
4064
4214
|
})
|
|
4065
4215
|
});
|
|
4066
4216
|
|
|
4067
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4217
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
4068
4218
|
var clientLoginRequest = import_zod13.z.object({
|
|
4069
4219
|
app_identifier: appIdentifier.meta({
|
|
4070
4220
|
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."
|
|
@@ -4086,6 +4236,49 @@ var clientLogoutRequest = import_zod13.z.object({
|
|
|
4086
4236
|
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."
|
|
4087
4237
|
})
|
|
4088
4238
|
});
|
|
4239
|
+
var clientLoginCodeRequest = import_zod13.z.object({
|
|
4240
|
+
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." }),
|
|
4241
|
+
email: import_zod13.z.email().meta({
|
|
4242
|
+
description: "The address to mail the code to, trimmed and compared case-insensitively. `202` whether or not it names an account of this app."
|
|
4243
|
+
})
|
|
4244
|
+
}).strict();
|
|
4245
|
+
var clientLoginCodeVerifyRequest = import_zod13.z.object({
|
|
4246
|
+
app_identifier: appIdentifier.meta({ description: "The app the code was requested for." }),
|
|
4247
|
+
email: import_zod13.z.email().meta({ description: "The address the code was mailed to, as typed when it was requested; trimmed and compared case-insensitively." }),
|
|
4248
|
+
code: loginCode.meta({ description: "The six digits from the mail, exactly \u2014 leading zeros included, no spaces." })
|
|
4249
|
+
}).strict();
|
|
4250
|
+
var twoFactorChallenge = import_zod13.z.object({
|
|
4251
|
+
status: import_zod13.z.enum(["two_factor_required", "two_factor_setup_required"]).meta({
|
|
4252
|
+
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."
|
|
4253
|
+
}),
|
|
4254
|
+
challenge: import_zod13.z.string().min(1).meta({
|
|
4255
|
+
description: "The handle the next step spends. Valid five minutes; afterwards it answers `410 token_spent` and the sign-in starts over."
|
|
4256
|
+
})
|
|
4257
|
+
});
|
|
4258
|
+
var clientSignInResult = import_zod13.z.union([sessionTokens, twoFactorChallenge]);
|
|
4259
|
+
var clientTwoFactorVerifyRequest = import_zod13.z.object({
|
|
4260
|
+
challenge: import_zod13.z.string().min(1).meta({ description: "The challenge the sign-in step answered." }),
|
|
4261
|
+
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." }),
|
|
4262
|
+
recovery_code: recoveryCode.optional().meta({ description: "One of the ten recovery codes, `xxxxx-xxxxx`, in either case. Spent by its use." })
|
|
4263
|
+
}).strict().refine((b) => b.code === void 0 !== (b.recovery_code === void 0), { message: "Send exactly one of code and recovery_code." });
|
|
4264
|
+
var clientTwoFactorSetupRequest = import_zod13.z.object({
|
|
4265
|
+
challenge: import_zod13.z.string().min(1).optional().meta({
|
|
4266
|
+
description: "The `two_factor_setup_required` challenge, during sign-in. Absent when the call carries the app user's bearer instead."
|
|
4267
|
+
})
|
|
4268
|
+
}).strict();
|
|
4269
|
+
var clientTwoFactorSetupConfirmRequest = import_zod13.z.object({
|
|
4270
|
+
challenge: import_zod13.z.string().min(1).optional().meta({ description: "The same challenge as at `setup`, during sign-in; absent with a bearer." }),
|
|
4271
|
+
code: totpCode.meta({ description: "A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it." })
|
|
4272
|
+
}).strict();
|
|
4273
|
+
var clientTwoFactorSetupConfirmResponse = import_zod13.z.object({
|
|
4274
|
+
recovery_codes: recoveryCodesList.meta({
|
|
4275
|
+
description: "The ten single-use recovery codes, lowercase, shown once. Any earlier set is void."
|
|
4276
|
+
}),
|
|
4277
|
+
session: sessionTokens.meta({ description: "The session the sign-in was waiting for, or a fresh one for the account settings." })
|
|
4278
|
+
});
|
|
4279
|
+
var clientTwoFactorDisableRequest = import_zod13.z.object({
|
|
4280
|
+
code: totpCode.meta({ description: "A code the authenticator shows now." })
|
|
4281
|
+
}).strict();
|
|
4089
4282
|
var clientRegisterRequest = import_zod13.z.object({
|
|
4090
4283
|
app_identifier: appIdentifier.meta({
|
|
4091
4284
|
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."
|
|
@@ -4093,8 +4286,8 @@ var clientRegisterRequest = import_zod13.z.object({
|
|
|
4093
4286
|
email: import_zod13.z.email().meta({
|
|
4094
4287
|
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."
|
|
4095
4288
|
}),
|
|
4096
|
-
password: password.meta({
|
|
4097
|
-
description: "The password for the new account
|
|
4289
|
+
password: password.optional().meta({
|
|
4290
|
+
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."
|
|
4098
4291
|
}),
|
|
4099
4292
|
display_name: import_zod13.z.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
|
|
4100
4293
|
description: "An optional human name for the account. The developer's own UI decides whether to ask for it."
|
|
@@ -4129,7 +4322,9 @@ var clientAcceptInvitationRequest = import_zod13.z.object({
|
|
|
4129
4322
|
token: import_zod13.z.string().min(1).meta({
|
|
4130
4323
|
description: "The opaque token from the invitation link, valid seven days. Unknown, expired, revoked and already-accepted all answer `410 token_spent`."
|
|
4131
4324
|
}),
|
|
4132
|
-
password: password.meta({
|
|
4325
|
+
password: password.optional().meta({
|
|
4326
|
+
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."
|
|
4327
|
+
}),
|
|
4133
4328
|
display_name: import_zod13.z.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
|
|
4134
4329
|
description: "An optional name, overriding whatever the invitation pre-filled."
|
|
4135
4330
|
})
|
|
@@ -4143,7 +4338,10 @@ var clientProviderListResponse = import_zod13.z.object({
|
|
|
4143
4338
|
slug: providerSlug.meta({ description: "The handle to put in the start URL: `GET /api/client/oidc/<slug>/start`." }),
|
|
4144
4339
|
name: import_zod13.z.string().meta({ description: "What to write on the button, as the developer configured it." })
|
|
4145
4340
|
})).meta({
|
|
4146
|
-
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
|
|
4341
|
+
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."
|
|
4342
|
+
}),
|
|
4343
|
+
sign_in_methods: appSignInMethods.meta({
|
|
4344
|
+
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."
|
|
4147
4345
|
})
|
|
4148
4346
|
});
|
|
4149
4347
|
var clientOidcStartQuery = import_zod13.z.object({
|
|
@@ -4192,7 +4390,8 @@ var clientOidcErrorCode = import_zod13.z.enum([
|
|
|
4192
4390
|
"provider_misconfigured",
|
|
4193
4391
|
"provider_disabled",
|
|
4194
4392
|
"invalid_request",
|
|
4195
|
-
"quota_exceeded"
|
|
4393
|
+
"quota_exceeded",
|
|
4394
|
+
"plan_limit"
|
|
4196
4395
|
]);
|
|
4197
4396
|
var clientMcpInteraction = import_zod13.z.object({
|
|
4198
4397
|
id: import_zod13.z.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." }),
|
|
@@ -4250,10 +4449,13 @@ var clientIdentity = import_zod13.z.object({
|
|
|
4250
4449
|
}),
|
|
4251
4450
|
email: import_zod13.z.email().nullable().meta({
|
|
4252
4451
|
description: "The address of the Fleetless user or app user behind this session, and `null` for a server key, which is not a person."
|
|
4452
|
+
}),
|
|
4453
|
+
two_factor_enabled: import_zod13.z.boolean().nullable().meta({
|
|
4454
|
+
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`."
|
|
4253
4455
|
})
|
|
4254
4456
|
});
|
|
4255
4457
|
|
|
4256
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4458
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/realtime.js
|
|
4257
4459
|
var clientAuth = import_zod14.z.object({
|
|
4258
4460
|
type: import_zod14.z.literal("auth"),
|
|
4259
4461
|
token: import_zod14.z.string().min(1)
|
|
@@ -4421,6 +4623,12 @@ var liveSessionEndReason = import_zod14.z.enum([
|
|
|
4421
4623
|
"expired",
|
|
4422
4624
|
/** The robot was deleted out from under the session. */
|
|
4423
4625
|
"robot_deleted",
|
|
4626
|
+
/**
|
|
4627
|
+
* The organization's live video for app users reached its plan's monthly
|
|
4628
|
+
* limit (2026-10-02, fleetless/fleetless#103); `detail` carries the
|
|
4629
|
+
* sentence the viewer shows.
|
|
4630
|
+
*/
|
|
4631
|
+
"plan_limit",
|
|
4424
4632
|
/**
|
|
4425
4633
|
* The cloud ended it and cannot say which of the above applied. **Kept
|
|
4426
4634
|
* deliberately**: a channel that cannot say "I do not know" will say
|
|
@@ -4531,34 +4739,59 @@ var orgEventDropped = import_zod14.z.object({
|
|
|
4531
4739
|
dropped: import_zod14.z.number().int().positive()
|
|
4532
4740
|
}).strict();
|
|
4533
4741
|
|
|
4534
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4742
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/feedback.js
|
|
4535
4743
|
var import_zod15 = require("zod");
|
|
4536
|
-
var
|
|
4744
|
+
var FEEDBACK_KINDS = ["idea", "problem", "question", "other"];
|
|
4745
|
+
var feedbackKind = import_zod15.z.enum(FEEDBACK_KINDS);
|
|
4746
|
+
var FEEDBACK_MESSAGE_MAX = 5e3;
|
|
4747
|
+
var feedbackRequest = import_zod15.z.object({
|
|
4748
|
+
kind: feedbackKind.meta({
|
|
4749
|
+
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."
|
|
4750
|
+
}),
|
|
4751
|
+
message: import_zod15.z.string().trim().min(1).max(FEEDBACK_MESSAGE_MAX).meta({
|
|
4752
|
+
description: "What the developer wrote, trimmed. At most 5000 characters; a message that is only whitespace is refused."
|
|
4753
|
+
}),
|
|
4754
|
+
page: import_zod15.z.string().startsWith("/").max(512).meta({
|
|
4755
|
+
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."
|
|
4756
|
+
})
|
|
4757
|
+
}).strict();
|
|
4758
|
+
var feedbackResponse = import_zod15.z.object({
|
|
4759
|
+
id: import_zod15.z.uuid().meta({
|
|
4760
|
+
description: "The stored message. It exists whatever `mail` says."
|
|
4761
|
+
}),
|
|
4762
|
+
mail: mailStatus.meta({
|
|
4763
|
+
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."
|
|
4764
|
+
})
|
|
4765
|
+
}).strict();
|
|
4766
|
+
|
|
4767
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/client-robots.js
|
|
4768
|
+
var import_zod16 = require("zod");
|
|
4769
|
+
var clientRobotListItem = import_zod16.z.object({
|
|
4537
4770
|
...robot.shape,
|
|
4538
4771
|
bridge_state: bridgeState.meta({
|
|
4539
4772
|
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."
|
|
4540
4773
|
}),
|
|
4541
|
-
published_version:
|
|
4774
|
+
published_version: import_zod16.z.number().int().positive().nullable().meta({
|
|
4542
4775
|
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.'
|
|
4543
4776
|
})
|
|
4544
4777
|
});
|
|
4545
|
-
var clientRobotListResponse =
|
|
4546
|
-
robots:
|
|
4778
|
+
var clientRobotListResponse = import_zod16.z.object({
|
|
4779
|
+
robots: import_zod16.z.array(clientRobotListItem).meta({
|
|
4547
4780
|
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."
|
|
4548
4781
|
})
|
|
4549
4782
|
});
|
|
4550
4783
|
|
|
4551
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4552
|
-
var
|
|
4553
|
-
var auditActor =
|
|
4554
|
-
kind:
|
|
4555
|
-
id:
|
|
4556
|
-
label:
|
|
4557
|
-
});
|
|
4558
|
-
var auditEvent =
|
|
4559
|
-
id:
|
|
4560
|
-
org_id:
|
|
4561
|
-
at:
|
|
4784
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/audit.js
|
|
4785
|
+
var import_zod17 = require("zod");
|
|
4786
|
+
var auditActor = import_zod17.z.object({
|
|
4787
|
+
kind: import_zod17.z.enum(["developer", "end_user", "app_user", "server_key", "bridge", "fleetless"]),
|
|
4788
|
+
id: import_zod17.z.uuid(),
|
|
4789
|
+
label: import_zod17.z.string().min(1).max(200)
|
|
4790
|
+
});
|
|
4791
|
+
var auditEvent = import_zod17.z.object({
|
|
4792
|
+
id: import_zod17.z.uuid(),
|
|
4793
|
+
org_id: import_zod17.z.uuid(),
|
|
4794
|
+
at: import_zod17.z.iso.datetime(),
|
|
4562
4795
|
/**
|
|
4563
4796
|
* A monotonic counter, ascending in write order, unique across the log.
|
|
4564
4797
|
*
|
|
@@ -4577,18 +4810,18 @@ var auditEvent = import_zod16.z.object({
|
|
|
4577
4810
|
* Required, not optional: an event without a sequence cannot be ordered
|
|
4578
4811
|
* against one that has it, and a log with two orderings has none.
|
|
4579
4812
|
*/
|
|
4580
|
-
seq:
|
|
4813
|
+
seq: import_zod17.z.number().int().positive(),
|
|
4581
4814
|
actor: auditActor,
|
|
4582
4815
|
/** Stable dotted name, e.g. `app_user.login`, `config.published`. */
|
|
4583
|
-
action:
|
|
4816
|
+
action: import_zod17.z.string().min(1).max(80),
|
|
4584
4817
|
/**
|
|
4585
4818
|
* What the action was about, if anything — a robot, an app, a user. Free
|
|
4586
4819
|
* of ids the console cannot resolve: carry the label with it.
|
|
4587
4820
|
*/
|
|
4588
|
-
target:
|
|
4589
|
-
kind:
|
|
4590
|
-
id:
|
|
4591
|
-
label:
|
|
4821
|
+
target: import_zod17.z.object({
|
|
4822
|
+
kind: import_zod17.z.string().min(1).max(40),
|
|
4823
|
+
id: import_zod17.z.string().min(1),
|
|
4824
|
+
label: import_zod17.z.string().min(1).max(200)
|
|
4592
4825
|
}).nullable(),
|
|
4593
4826
|
/**
|
|
4594
4827
|
* Action-specific extras.
|
|
@@ -4602,10 +4835,10 @@ var auditEvent = import_zod16.z.object({
|
|
|
4602
4835
|
* So: never credentials, never tokens. That is a rule, not a guarantee the
|
|
4603
4836
|
* schema enforces.
|
|
4604
4837
|
*/
|
|
4605
|
-
details:
|
|
4838
|
+
details: import_zod17.z.record(import_zod17.z.string(), import_zod17.z.unknown()).nullable()
|
|
4606
4839
|
});
|
|
4607
4840
|
var auditTimestampMs = wireTimestampMs;
|
|
4608
|
-
var auditQuery =
|
|
4841
|
+
var auditQuery = import_zod17.z.object({
|
|
4609
4842
|
/** Only events with a smaller `seq` — the next, older page. */
|
|
4610
4843
|
before_seq: wireSeqCursor.optional(),
|
|
4611
4844
|
/**
|
|
@@ -4614,9 +4847,9 @@ var auditQuery = import_zod16.z.object({
|
|
|
4614
4847
|
* coercion's result in either `io` direction, so the artifact would describe
|
|
4615
4848
|
* a shape a query string can never carry.
|
|
4616
4849
|
*/
|
|
4617
|
-
limit:
|
|
4850
|
+
limit: import_zod17.z.union([import_zod17.z.string().regex(/^\d{1,4}$/), import_zod17.z.number().int()]).transform((v) => Number(v)).pipe(import_zod17.z.number().int().positive().max(500)).optional(),
|
|
4618
4851
|
/** Exact action name, e.g. `config.published`. No prefix matching: a filter that matches more than it says is not one. */
|
|
4619
|
-
action:
|
|
4852
|
+
action: import_zod17.z.string().min(1).max(80).optional(),
|
|
4620
4853
|
/**
|
|
4621
4854
|
* Everything under a dotted prefix, e.g. `server_key.` for all three
|
|
4622
4855
|
* server-key actions.
|
|
@@ -4633,7 +4866,7 @@ var auditQuery = import_zod16.z.object({
|
|
|
4633
4866
|
* happily. The cloud is the only enforcement point — the same residual
|
|
4634
4867
|
* `orgLatencyQuery` and `orgUsageQuery` already name.
|
|
4635
4868
|
*/
|
|
4636
|
-
action_prefix:
|
|
4869
|
+
action_prefix: import_zod17.z.string().min(1).max(80).optional(),
|
|
4637
4870
|
/**
|
|
4638
4871
|
* Only events by this actor.
|
|
4639
4872
|
*
|
|
@@ -4645,9 +4878,21 @@ var auditQuery = import_zod16.z.object({
|
|
|
4645
4878
|
* Not an injection question — the query is parameterised either way. It is a
|
|
4646
4879
|
* **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
|
|
4647
4880
|
*/
|
|
4648
|
-
actor_id:
|
|
4881
|
+
actor_id: import_zod17.z.uuid().optional(),
|
|
4649
4882
|
/** Only events about this kind of target, e.g. `robot`. */
|
|
4650
|
-
target_kind:
|
|
4883
|
+
target_kind: import_zod17.z.string().min(1).max(40).optional(),
|
|
4884
|
+
/**
|
|
4885
|
+
* Only events about this target — and, for a robot, also the events that
|
|
4886
|
+
* name it in `details.robot_id`, so a robot's log includes what was
|
|
4887
|
+
* started on it.
|
|
4888
|
+
*
|
|
4889
|
+
* **A string, not `z.uuid()`**, unlike `actor_id` above: `target.id` is a
|
|
4890
|
+
* string in this contract and text in the cloud's table, so a uuid rule
|
|
4891
|
+
* here would refuse ids the log can hold, and no value can fail a cast.
|
|
4892
|
+
*/
|
|
4893
|
+
target_id: import_zod17.z.string().min(1).max(200).optional().meta({
|
|
4894
|
+
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."
|
|
4895
|
+
}),
|
|
4651
4896
|
/**
|
|
4652
4897
|
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
4653
4898
|
* same rule the history shapes follow.
|
|
@@ -4663,8 +4908,8 @@ var auditQuery = import_zod16.z.object({
|
|
|
4663
4908
|
message: "action and action_prefix cannot be combined",
|
|
4664
4909
|
path: ["action_prefix"]
|
|
4665
4910
|
});
|
|
4666
|
-
var auditListResponse =
|
|
4667
|
-
events:
|
|
4911
|
+
var auditListResponse = import_zod17.z.object({
|
|
4912
|
+
events: import_zod17.z.array(auditEvent),
|
|
4668
4913
|
/**
|
|
4669
4914
|
* The `seq` a caller sends as `before_seq` to keep reading — or `null` when
|
|
4670
4915
|
* there is nothing further.
|
|
@@ -4675,32 +4920,361 @@ var auditListResponse = import_zod16.z.object({
|
|
|
4675
4920
|
* not mean *no more* here. The same distinction `historySamples` was given
|
|
4676
4921
|
* `truncated` for.
|
|
4677
4922
|
*/
|
|
4678
|
-
next_cursor:
|
|
4923
|
+
next_cursor: import_zod17.z.number().int().positive().nullable()
|
|
4679
4924
|
});
|
|
4680
4925
|
|
|
4681
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4682
|
-
var
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4926
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/errors.js
|
|
4927
|
+
var import_zod19 = require("zod");
|
|
4928
|
+
|
|
4929
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/plans.js
|
|
4930
|
+
var import_zod18 = require("zod");
|
|
4931
|
+
var planId = import_zod18.z.enum(["basic", "plus", "pro", "enterprise"]);
|
|
4932
|
+
var planLimitKey = import_zod18.z.enum([
|
|
4933
|
+
"seats",
|
|
4934
|
+
"robots",
|
|
4935
|
+
"apps",
|
|
4936
|
+
"app_users",
|
|
4937
|
+
"live_video_ms_per_month",
|
|
4938
|
+
"asset_bytes_per_robot",
|
|
4939
|
+
"history_days",
|
|
4940
|
+
"audit_days"
|
|
4941
|
+
]);
|
|
4942
|
+
var limit = import_zod18.z.number().int().positive().nullable();
|
|
4943
|
+
var planLimits = import_zod18.z.object({
|
|
4944
|
+
seats: limit.meta({ description: "Developers, owners included, plus pending team invitations." }),
|
|
4945
|
+
robots: limit.meta({ description: "Robots in the org." }),
|
|
4946
|
+
apps: limit.meta({ description: "Apps in the org." }),
|
|
4947
|
+
app_users: limit.meta({ description: "App users across every app of the org, plus pending app-user invitations." }),
|
|
4948
|
+
live_video_ms_per_month: limit.meta({
|
|
4949
|
+
description: "Live video watched by app users in one UTC calendar month, in milliseconds, across the org. Console sessions do not count."
|
|
4950
|
+
}),
|
|
4951
|
+
asset_bytes_per_robot: limit.meta({
|
|
4952
|
+
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."
|
|
4953
|
+
}),
|
|
4954
|
+
history_days: limit.meta({ description: "Days the org's robot history is kept." }),
|
|
4955
|
+
audit_days: limit.meta({ description: "Days the org's audit log is kept." })
|
|
4956
|
+
});
|
|
4957
|
+
var planFeature = import_zod18.z.enum(["app_mcp", "two_factor", "require_two_factor", "app_oidc", "hosted_logo", "audit_export", "addons"]);
|
|
4958
|
+
var FEATURE_DESCRIPTIONS = {
|
|
4959
|
+
app_mcp: "An app's own MCP endpoint, for its app users.",
|
|
4960
|
+
two_factor: "Two-factor sign-in for developers.",
|
|
4961
|
+
require_two_factor: "An owner may require two-factor sign-in for every developer of the org.",
|
|
4962
|
+
app_oidc: "An app may let its users sign in through an OpenID Connect identity provider.",
|
|
4963
|
+
hosted_logo: "An app's hosted pages show its logo and accent colour, not only its name.",
|
|
4964
|
+
audit_export: "The audit log can be exported as CSV.",
|
|
4965
|
+
addons: "Add-ons can be bought on top of the plan."
|
|
4966
|
+
};
|
|
4967
|
+
var planFeatures = import_zod18.z.object(Object.fromEntries(planFeature.options.map((f) => [f, import_zod18.z.boolean().meta({ description: FEATURE_DESCRIPTIONS[f] })])));
|
|
4968
|
+
var cents = import_zod18.z.number().int().nonnegative();
|
|
4969
|
+
var planPrices = import_zod18.z.object({
|
|
4970
|
+
eur_month: cents.meta({ description: "Per month in euro cents, excluding VAT." }),
|
|
4971
|
+
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." }),
|
|
4972
|
+
eur_year: cents.meta({ description: "Per year in euro cents, excluding VAT: twelve months less 15 %." }),
|
|
4973
|
+
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." })
|
|
4974
|
+
});
|
|
4975
|
+
var planSupport = import_zod18.z.enum(["community", "email", "priority", "named_contact"]);
|
|
4976
|
+
var planCatalogueEntry = import_zod18.z.object({
|
|
4977
|
+
id: planId.meta({ description: "The plan." }),
|
|
4978
|
+
name: import_zod18.z.string().min(1).meta({ description: "The plan's display name: Basic, Plus, Pro or Enterprise." }),
|
|
4979
|
+
limits: planLimits.meta({ description: "What the plan allows. `null` means by contract." }),
|
|
4980
|
+
features: planFeatures.meta({ description: "What the plan unlocks." }),
|
|
4981
|
+
prices: planPrices.nullable().meta({ description: "`null` for Enterprise: sold by contract, on request." }),
|
|
4982
|
+
support: planSupport.meta({ description: "The support that comes with the plan." })
|
|
4983
|
+
});
|
|
4984
|
+
var addonKey = import_zod18.z.enum(["seats", "robots", "apps", "app_user_packs", "live_video_packs"]);
|
|
4985
|
+
var addonCatalogueEntry = import_zod18.z.object({
|
|
4986
|
+
key: addonKey.meta({ description: "The add-on." }),
|
|
4987
|
+
raises: planLimitKey.meta({ description: "The plan limit this add-on raises." }),
|
|
4988
|
+
per_unit: import_zod18.z.number().int().positive().meta({ description: "How much one unit of this add-on raises `raises` by." }),
|
|
4989
|
+
prices: planPrices.meta({ description: "The price of one unit." })
|
|
4990
|
+
});
|
|
4991
|
+
function usdCentsFromEurCents(eurCents) {
|
|
4992
|
+
return Math.floor((eurCents * 115 + 9999) / 1e4) * 100;
|
|
4993
|
+
}
|
|
4994
|
+
function yearlyEurCents(monthEurCents) {
|
|
4995
|
+
return Math.round(monthEurCents * 12 * 85 / 100);
|
|
4996
|
+
}
|
|
4997
|
+
function pricesFromEurMonth(eurMonthCents) {
|
|
4998
|
+
const eurYear = yearlyEurCents(eurMonthCents);
|
|
4999
|
+
return {
|
|
5000
|
+
eur_month: eurMonthCents,
|
|
5001
|
+
usd_month: usdCentsFromEurCents(eurMonthCents),
|
|
5002
|
+
eur_year: eurYear,
|
|
5003
|
+
usd_year: usdCentsFromEurCents(eurYear)
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
var PLANS = {
|
|
5007
|
+
basic: {
|
|
5008
|
+
id: "basic",
|
|
5009
|
+
name: "Basic",
|
|
5010
|
+
limits: {
|
|
5011
|
+
seats: 1,
|
|
5012
|
+
robots: 1,
|
|
5013
|
+
apps: 1,
|
|
5014
|
+
app_users: 10,
|
|
5015
|
+
live_video_ms_per_month: 36e6,
|
|
5016
|
+
asset_bytes_per_robot: 1e9,
|
|
5017
|
+
history_days: 7,
|
|
5018
|
+
audit_days: 7
|
|
5019
|
+
},
|
|
5020
|
+
features: {
|
|
5021
|
+
app_mcp: false,
|
|
5022
|
+
two_factor: false,
|
|
5023
|
+
require_two_factor: false,
|
|
5024
|
+
app_oidc: false,
|
|
5025
|
+
hosted_logo: false,
|
|
5026
|
+
audit_export: false,
|
|
5027
|
+
addons: false
|
|
5028
|
+
},
|
|
5029
|
+
prices: pricesFromEurMonth(0),
|
|
5030
|
+
support: "community"
|
|
5031
|
+
},
|
|
5032
|
+
plus: {
|
|
5033
|
+
id: "plus",
|
|
5034
|
+
name: "Plus",
|
|
5035
|
+
limits: {
|
|
5036
|
+
seats: 3,
|
|
5037
|
+
robots: 3,
|
|
5038
|
+
apps: 3,
|
|
5039
|
+
app_users: 25,
|
|
5040
|
+
live_video_ms_per_month: 36e7,
|
|
5041
|
+
asset_bytes_per_robot: 2e9,
|
|
5042
|
+
history_days: 30,
|
|
5043
|
+
audit_days: 30
|
|
5044
|
+
},
|
|
5045
|
+
features: {
|
|
5046
|
+
app_mcp: true,
|
|
5047
|
+
two_factor: true,
|
|
5048
|
+
require_two_factor: false,
|
|
5049
|
+
app_oidc: true,
|
|
5050
|
+
hosted_logo: true,
|
|
5051
|
+
audit_export: false,
|
|
5052
|
+
addons: false
|
|
5053
|
+
},
|
|
5054
|
+
prices: pricesFromEurMonth(2900),
|
|
5055
|
+
support: "email"
|
|
5056
|
+
},
|
|
5057
|
+
pro: {
|
|
5058
|
+
id: "pro",
|
|
5059
|
+
name: "Pro",
|
|
5060
|
+
limits: {
|
|
5061
|
+
seats: 5,
|
|
5062
|
+
robots: 5,
|
|
5063
|
+
apps: 5,
|
|
5064
|
+
app_users: 50,
|
|
5065
|
+
live_video_ms_per_month: 9e8,
|
|
5066
|
+
asset_bytes_per_robot: 3e9,
|
|
5067
|
+
history_days: 90,
|
|
5068
|
+
audit_days: 90
|
|
5069
|
+
},
|
|
5070
|
+
features: {
|
|
5071
|
+
app_mcp: true,
|
|
5072
|
+
two_factor: true,
|
|
5073
|
+
require_two_factor: true,
|
|
5074
|
+
app_oidc: true,
|
|
5075
|
+
hosted_logo: true,
|
|
5076
|
+
audit_export: true,
|
|
5077
|
+
addons: true
|
|
5078
|
+
},
|
|
5079
|
+
prices: pricesFromEurMonth(14900),
|
|
5080
|
+
support: "priority"
|
|
5081
|
+
},
|
|
5082
|
+
enterprise: {
|
|
5083
|
+
id: "enterprise",
|
|
5084
|
+
name: "Enterprise",
|
|
5085
|
+
limits: {
|
|
5086
|
+
seats: null,
|
|
5087
|
+
robots: null,
|
|
5088
|
+
apps: null,
|
|
5089
|
+
app_users: null,
|
|
5090
|
+
live_video_ms_per_month: null,
|
|
5091
|
+
asset_bytes_per_robot: null,
|
|
5092
|
+
history_days: null,
|
|
5093
|
+
audit_days: null
|
|
5094
|
+
},
|
|
5095
|
+
features: {
|
|
5096
|
+
app_mcp: true,
|
|
5097
|
+
two_factor: true,
|
|
5098
|
+
require_two_factor: true,
|
|
5099
|
+
app_oidc: true,
|
|
5100
|
+
hosted_logo: true,
|
|
5101
|
+
audit_export: true,
|
|
5102
|
+
addons: true
|
|
5103
|
+
},
|
|
5104
|
+
prices: null,
|
|
5105
|
+
support: "named_contact"
|
|
5106
|
+
}
|
|
5107
|
+
};
|
|
5108
|
+
var ADDONS = {
|
|
5109
|
+
seats: { key: "seats", raises: "seats", per_unit: 1, prices: pricesFromEurMonth(900) },
|
|
5110
|
+
robots: { key: "robots", raises: "robots", per_unit: 1, prices: pricesFromEurMonth(1900) },
|
|
5111
|
+
apps: { key: "apps", raises: "apps", per_unit: 1, prices: pricesFromEurMonth(900) },
|
|
5112
|
+
app_user_packs: { key: "app_user_packs", raises: "app_users", per_unit: 5, prices: pricesFromEurMonth(1e3) },
|
|
5113
|
+
live_video_packs: { key: "live_video_packs", raises: "live_video_ms_per_month", per_unit: 9e8, prices: pricesFromEurMonth(900) }
|
|
5114
|
+
};
|
|
5115
|
+
var planCurrency = import_zod18.z.enum(["eur", "usd"]);
|
|
5116
|
+
var addonCount = import_zod18.z.number().int().nonnegative();
|
|
5117
|
+
var orgAddons = import_zod18.z.object({
|
|
5118
|
+
seats: addonCount.meta({ description: "Extra developer seats, one each." }),
|
|
5119
|
+
robots: addonCount.meta({ description: "Extra robots, one each." }),
|
|
5120
|
+
apps: addonCount.meta({ description: "Extra apps, one each." }),
|
|
5121
|
+
app_user_packs: addonCount.meta({ description: "Packs of five extra app users." }),
|
|
5122
|
+
live_video_packs: addonCount.meta({ description: "Packs of 250 extra hours of app-user live video per month." })
|
|
5123
|
+
});
|
|
5124
|
+
var usageCount = import_zod18.z.number().int().nonnegative();
|
|
5125
|
+
var orgPlanUsage = import_zod18.z.object({
|
|
5126
|
+
seats: usageCount.meta({ description: "Developers, owners included, plus pending team invitations." }),
|
|
5127
|
+
robots: usageCount.meta({ description: "Robots in the org." }),
|
|
5128
|
+
apps: usageCount.meta({ description: "Apps in the org." }),
|
|
5129
|
+
app_users: usageCount.meta({ description: "App users across every app, plus pending app-user invitations." }),
|
|
5130
|
+
live_video_ms_this_month: usageCount.meta({
|
|
5131
|
+
description: "Live video watched by app users in the current UTC calendar month, in milliseconds. Console sessions do not count."
|
|
5132
|
+
}),
|
|
5133
|
+
asset_bytes: usageCount.meta({ description: "Bytes the assets of every robot of the org occupy, together." })
|
|
5134
|
+
});
|
|
5135
|
+
var planChangeKeep = import_zod18.z.object({
|
|
5136
|
+
robots: import_zod18.z.array(import_zod18.z.uuid()).meta({ description: "The robots that stay, by id. Every other robot is deleted when the change takes effect." }),
|
|
5137
|
+
apps: import_zod18.z.array(import_zod18.z.uuid()).meta({ description: "The apps that stay, by id. Every other app is deleted when the change takes effect." }),
|
|
5138
|
+
app_users: import_zod18.z.array(import_zod18.z.uuid()).meta({
|
|
5139
|
+
description: "The app users that stay, by id, across every app. Every other app user is deleted when the change takes effect."
|
|
5140
|
+
}),
|
|
5141
|
+
developers: import_zod18.z.array(import_zod18.z.uuid()).meta({
|
|
5142
|
+
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."
|
|
5143
|
+
})
|
|
5144
|
+
}).strict();
|
|
5145
|
+
var planChangeReason = import_zod18.z.enum(["downgrade", "cancel", "migration", "lock"]);
|
|
5146
|
+
var pendingPlanChange = import_zod18.z.object({
|
|
5147
|
+
target_plan: planId.meta({ description: "The plan the org moves to." }),
|
|
5148
|
+
reason: planChangeReason.meta({ description: "Why the change is pending: `downgrade`, `cancel`, `migration` or `lock`." }),
|
|
5149
|
+
effective_at: import_zod18.z.iso.datetime().nullable().meta({
|
|
5150
|
+
description: "When the change takes effect. `null` only for a move off the beta whose date is not set yet."
|
|
5151
|
+
}),
|
|
5152
|
+
keep: planChangeKeep.nullable().meta({
|
|
5153
|
+
description: "What stays. `null` when the org already fits the target plan and nothing is deleted."
|
|
5154
|
+
}),
|
|
5155
|
+
history_days_after: import_zod18.z.number().int().positive().meta({
|
|
5156
|
+
description: "Days of history and audit log the org keeps on the target plan."
|
|
5157
|
+
}),
|
|
5158
|
+
chosen_by: import_zod18.z.uuid().meta({ description: "The user id of the owner who chose the change, or the nil UUID when Fleetless queued it." }),
|
|
5159
|
+
chosen_at: import_zod18.z.iso.datetime().meta({ description: "When the change was chosen." })
|
|
5160
|
+
});
|
|
5161
|
+
var orgLock = import_zod18.z.object({
|
|
5162
|
+
reason: import_zod18.z.enum(["payment", "migration"]).meta({
|
|
5163
|
+
description: "`payment`: a payment is missing. `migration`: the org did not choose what stays when the beta ended."
|
|
5164
|
+
}),
|
|
5165
|
+
since: import_zod18.z.iso.datetime().meta({ description: "When the org was locked." })
|
|
5166
|
+
});
|
|
5167
|
+
var orgPlan = import_zod18.z.object({
|
|
5168
|
+
plan: planId.meta({ description: "The org's current plan." }),
|
|
5169
|
+
currency: planCurrency.meta({ description: "The currency the org's prices are shown and billed in." }),
|
|
5170
|
+
period_ends_at: import_zod18.z.iso.datetime().meta({
|
|
5171
|
+
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."
|
|
5172
|
+
}),
|
|
5173
|
+
addons: orgAddons.meta({ description: "The add-ons the org has bought. All zero on a plan without the `addons` feature." }),
|
|
5174
|
+
limits: planLimits.meta({
|
|
5175
|
+
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`."
|
|
5176
|
+
}),
|
|
5177
|
+
features: planFeatures.meta({ description: "What the org's plan unlocks." }),
|
|
5178
|
+
usage: orgPlanUsage.meta({ description: "What the org uses now, counted the way each limit counts it." }),
|
|
5179
|
+
pending_change: pendingPlanChange.nullable().meta({ description: "A move to a lower plan that has not taken effect yet, or `null`." }),
|
|
5180
|
+
lock: orgLock.nullable().meta({ description: "Why and since when the org is locked, or `null` when it is not." }),
|
|
5181
|
+
switch: import_zod18.z.object({
|
|
5182
|
+
at: import_zod18.z.iso.datetime().nullable().meta({
|
|
5183
|
+
description: "When the org moves from the beta onto its plan. `null` while the date is not set."
|
|
5184
|
+
}),
|
|
5185
|
+
needs_choice: import_zod18.z.boolean().meta({
|
|
5186
|
+
description: "Whether the org uses more than Basic allows, so an owner has to choose what stays before `at`."
|
|
5187
|
+
})
|
|
5188
|
+
}).nullable().meta({ description: "Set only while the org is still on the beta; `null` for every other org." })
|
|
4687
5189
|
});
|
|
4688
|
-
var
|
|
4689
|
-
|
|
5190
|
+
var planChangeRequest = import_zod18.z.object({
|
|
5191
|
+
target_plan: planId.meta({ description: "The lower plan to move to; `basic` cancels." }),
|
|
5192
|
+
keep: planChangeKeep.nullable().meta({
|
|
5193
|
+
description: "What stays. `null` when the org already fits the target plan, so nothing is deleted."
|
|
5194
|
+
})
|
|
5195
|
+
}).strict();
|
|
5196
|
+
var planOverrides = import_zod18.z.object(Object.fromEntries(planLimitKey.options.map((k) => [
|
|
5197
|
+
k,
|
|
5198
|
+
limit.optional().meta({ description: `Replaces the plan's \`${k}\`. \`null\` clears the override.` })
|
|
5199
|
+
]))).strict();
|
|
5200
|
+
var adminPlanChangeRequest = import_zod18.z.object({
|
|
5201
|
+
plan: planId.meta({ description: "The plan the org is on after this request." }),
|
|
5202
|
+
addons: orgAddons.partial().strict().optional().meta({ description: "Add-on counts to set. Counts not named stay as they are." }),
|
|
5203
|
+
overrides: planOverrides.optional().meta({ description: "Limits to override. `null` clears an override." }),
|
|
5204
|
+
currency: planCurrency.optional().meta({ description: "The currency the org is billed in." }),
|
|
5205
|
+
period_ends_at: import_zod18.z.iso.datetime().nullable().optional().meta({
|
|
5206
|
+
description: "The end of the org's billing period. `null` falls back to the end of the current UTC month."
|
|
5207
|
+
})
|
|
5208
|
+
}).strict();
|
|
5209
|
+
|
|
5210
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/errors.js
|
|
5211
|
+
var apiError = import_zod19.z.object({
|
|
5212
|
+
code: import_zod19.z.string().min(1),
|
|
5213
|
+
message: import_zod19.z.string().min(1),
|
|
5214
|
+
details: import_zod19.z.unknown().optional()
|
|
5215
|
+
});
|
|
5216
|
+
var parameterViolation = import_zod19.z.object({
|
|
5217
|
+
field: import_zod19.z.string().min(1),
|
|
4690
5218
|
/** Which rule failed — `min`, `max`, `enum`, `pattern`, `required`, `undeclared`. */
|
|
4691
|
-
rule:
|
|
4692
|
-
message:
|
|
5219
|
+
rule: import_zod19.z.string().min(1),
|
|
5220
|
+
message: import_zod19.z.string().min(1)
|
|
5221
|
+
});
|
|
5222
|
+
var parameterInvalidDetails = import_zod19.z.object({
|
|
5223
|
+
violations: import_zod19.z.array(parameterViolation).min(1)
|
|
5224
|
+
});
|
|
5225
|
+
var cancelRejectedDetails = import_zod19.z.object({
|
|
5226
|
+
goals: import_zod19.z.array(bridgeCancelResultEntry).min(1)
|
|
5227
|
+
});
|
|
5228
|
+
var invalidCodeDetails = import_zod19.z.object({
|
|
5229
|
+
attempts_left: import_zod19.z.number().int().min(0).meta({
|
|
5230
|
+
description: "How many more wrong codes this code or challenge takes before it is spent. `0` means the next attempt answers `410 token_spent`."
|
|
5231
|
+
})
|
|
5232
|
+
});
|
|
5233
|
+
var planLimitDetails = import_zod19.z.object({
|
|
5234
|
+
limit: planLimitKey.exclude(["history_days", "audit_days"]).meta({ description: "The limit the action would exceed." }),
|
|
5235
|
+
used: import_zod19.z.number().int().nonnegative().meta({ description: "How much of the limit the org uses now." }),
|
|
5236
|
+
max: import_zod19.z.number().int().nonnegative().meta({
|
|
5237
|
+
description: "The limit. For `asset_bytes_per_robot`, the org's whole pool: robots \xD7 the plan's bytes per robot."
|
|
5238
|
+
}),
|
|
5239
|
+
plan: planId.meta({ description: "The plan whose limit refused: the target plan while a move to a lower plan is pending." }),
|
|
5240
|
+
lifted_by: import_zod19.z.object({
|
|
5241
|
+
plan: planId.nullable().meta({ description: "The cheapest higher plan that raises this limit, or `null` when none does." }),
|
|
5242
|
+
addon: addonKey.nullable().meta({
|
|
5243
|
+
description: "The add-on that raises this limit, when the org's plan can buy add-ons; otherwise `null`."
|
|
5244
|
+
})
|
|
5245
|
+
}).meta({ description: "What would lift the limit." })
|
|
5246
|
+
});
|
|
5247
|
+
var assetPlanLimitDetails = planLimitDetails.extend({
|
|
5248
|
+
...assetStoreRefusedDetails.shape,
|
|
5249
|
+
store_bytes: assetStoreRefusedDetails.shape.store_bytes.meta({
|
|
5250
|
+
description: "The org's asset pool, in bytes: `robots \xD7 asset_bytes_per_robot`."
|
|
5251
|
+
}),
|
|
5252
|
+
used_bytes: assetStoreRefusedDetails.shape.used_bytes.meta({
|
|
5253
|
+
description: "Bytes the org's assets occupy, across every robot, before this upload."
|
|
5254
|
+
})
|
|
4693
5255
|
});
|
|
4694
|
-
var
|
|
4695
|
-
|
|
5256
|
+
var planRequiredDetails = import_zod19.z.object({
|
|
5257
|
+
feature: planFeature.meta({ description: "The feature the action needs." }),
|
|
5258
|
+
plan: planId.meta({ description: "The org's current plan." }),
|
|
5259
|
+
required_plan: planId.meta({ description: "The cheapest plan that has the feature." })
|
|
5260
|
+
});
|
|
5261
|
+
var orgLockedDetails = import_zod19.z.object({
|
|
5262
|
+
reason: import_zod19.z.enum(["payment", "migration"]).meta({
|
|
5263
|
+
description: "`payment`: a payment is missing. `migration`: the org did not choose what stays when the beta ended."
|
|
5264
|
+
})
|
|
4696
5265
|
});
|
|
4697
|
-
var
|
|
4698
|
-
|
|
5266
|
+
var fileTooLargeDetails = import_zod19.z.object({
|
|
5267
|
+
max_bytes: import_zod19.z.number().int().positive().meta({
|
|
5268
|
+
description: "The most one asset file can be, in bytes: `ASSET_FILE_MAX_BYTES`, the same on every plan."
|
|
5269
|
+
}),
|
|
5270
|
+
size_bytes: import_zod19.z.number().int().positive().nullable().meta({
|
|
5271
|
+
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."
|
|
5272
|
+
})
|
|
4699
5273
|
});
|
|
4700
5274
|
|
|
4701
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4702
|
-
var
|
|
4703
|
-
var oauthErrorCode =
|
|
5275
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/oauth.js
|
|
5276
|
+
var import_zod20 = require("zod");
|
|
5277
|
+
var oauthErrorCode = import_zod20.z.enum([
|
|
4704
5278
|
"invalid_request",
|
|
4705
5279
|
"invalid_client",
|
|
4706
5280
|
"invalid_grant",
|
|
@@ -4713,11 +5287,11 @@ var oauthErrorCode = import_zod18.z.enum([
|
|
|
4713
5287
|
/** RFC 8707: the `resource` named is not one this server issues tokens for. */
|
|
4714
5288
|
"invalid_target"
|
|
4715
5289
|
]);
|
|
4716
|
-
var oauthError =
|
|
5290
|
+
var oauthError = import_zod20.z.object({
|
|
4717
5291
|
error: oauthErrorCode,
|
|
4718
|
-
error_description:
|
|
5292
|
+
error_description: import_zod20.z.string().min(1).max(500).optional(),
|
|
4719
5293
|
/** Echoed back per RFC 6749 §4.1.2.1 so a client can match the response. */
|
|
4720
|
-
state:
|
|
5294
|
+
state: import_zod20.z.string().min(1).max(500).optional(),
|
|
4721
5295
|
/**
|
|
4722
5296
|
* **A Fleetless reason carried inside a standard envelope, and it exists
|
|
4723
5297
|
* because the alternative lost a distinction.**
|
|
@@ -4735,9 +5309,9 @@ var oauthError = import_zod18.z.object({
|
|
|
4735
5309
|
* our own tooling switches on. RFC 6749 §5.2 permits additional members, and
|
|
4736
5310
|
* a client that ignores this one still behaves correctly.
|
|
4737
5311
|
*/
|
|
4738
|
-
fleetless_code:
|
|
5312
|
+
fleetless_code: import_zod20.z.string().min(1).max(60).optional()
|
|
4739
5313
|
});
|
|
4740
|
-
var redirectUri =
|
|
5314
|
+
var redirectUri = import_zod20.z.string().min(1).max(2e3).refine((v) => {
|
|
4741
5315
|
let url;
|
|
4742
5316
|
try {
|
|
4743
5317
|
url = new URL(v);
|
|
@@ -4752,180 +5326,180 @@ var redirectUri = import_zod18.z.string().min(1).max(2e3).refine((v) => {
|
|
|
4752
5326
|
return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
4753
5327
|
return false;
|
|
4754
5328
|
}, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
|
|
4755
|
-
var codeChallengeMethod =
|
|
5329
|
+
var codeChallengeMethod = import_zod20.z.enum(["S256"]);
|
|
4756
5330
|
var MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
4757
|
-
var dynamicClientRegistrationRequest =
|
|
4758
|
-
redirect_uris:
|
|
5331
|
+
var dynamicClientRegistrationRequest = import_zod20.z.object({
|
|
5332
|
+
redirect_uris: import_zod20.z.array(redirectUri).min(1).max(MCP_DCR_MAX_REDIRECT_URIS).meta({
|
|
4759
5333
|
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.`
|
|
4760
5334
|
}),
|
|
4761
|
-
client_name:
|
|
5335
|
+
client_name: import_zod20.z.string().min(1).max(200).optional().meta({
|
|
4762
5336
|
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"*.'
|
|
4763
5337
|
}),
|
|
4764
|
-
token_endpoint_auth_method:
|
|
5338
|
+
token_endpoint_auth_method: import_zod20.z.enum(["none"]).optional().meta({
|
|
4765
5339
|
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."
|
|
4766
5340
|
}),
|
|
4767
|
-
grant_types:
|
|
5341
|
+
grant_types: import_zod20.z.array(import_zod20.z.enum(["authorization_code", "refresh_token"])).optional().meta({
|
|
4768
5342
|
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."
|
|
4769
5343
|
}),
|
|
4770
|
-
response_types:
|
|
5344
|
+
response_types: import_zod20.z.array(import_zod20.z.enum(["code"])).optional().meta({
|
|
4771
5345
|
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."
|
|
4772
5346
|
}),
|
|
4773
|
-
scope:
|
|
5347
|
+
scope: import_zod20.z.string().max(500).optional().meta({
|
|
4774
5348
|
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."
|
|
4775
5349
|
})
|
|
4776
5350
|
}).meta({
|
|
4777
5351
|
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)."
|
|
4778
5352
|
});
|
|
4779
|
-
var dynamicClientRegistrationResponse =
|
|
4780
|
-
client_id:
|
|
5353
|
+
var dynamicClientRegistrationResponse = import_zod20.z.object({
|
|
5354
|
+
client_id: import_zod20.z.string().min(1).max(200).meta({
|
|
4781
5355
|
description: "The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier."
|
|
4782
5356
|
}),
|
|
4783
|
-
client_name:
|
|
5357
|
+
client_name: import_zod20.z.string().min(1).max(200).meta({
|
|
4784
5358
|
description: "The name the client registered under, echoed back. Chosen by the client and not vouched for by Fleetless."
|
|
4785
5359
|
}),
|
|
4786
|
-
redirect_uris:
|
|
5360
|
+
redirect_uris: import_zod20.z.array(redirectUri).meta({
|
|
4787
5361
|
description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
|
|
4788
5362
|
}),
|
|
4789
|
-
grant_types:
|
|
5363
|
+
grant_types: import_zod20.z.array(import_zod20.z.string()).meta({
|
|
4790
5364
|
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.'
|
|
4791
5365
|
}),
|
|
4792
|
-
response_types:
|
|
5366
|
+
response_types: import_zod20.z.array(import_zod20.z.string()).meta({
|
|
4793
5367
|
description: "The response types this client may ask for: `code`."
|
|
4794
5368
|
}),
|
|
4795
|
-
token_endpoint_auth_method:
|
|
5369
|
+
token_endpoint_auth_method: import_zod20.z.literal("none").meta({
|
|
4796
5370
|
description: "`none` \u2014 this server registers public clients only, and PKCE rather than a secret is what protects the exchange."
|
|
4797
5371
|
}),
|
|
4798
|
-
client_id_issued_at:
|
|
5372
|
+
client_id_issued_at: import_zod20.z.number().int().nonnegative().meta({
|
|
4799
5373
|
description: "When the registration was created, in seconds since the epoch, per RFC 7591."
|
|
4800
5374
|
}),
|
|
4801
|
-
client_secret_expires_at:
|
|
5375
|
+
client_secret_expires_at: import_zod20.z.literal(0).meta({
|
|
4802
5376
|
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."
|
|
4803
5377
|
})
|
|
4804
5378
|
});
|
|
4805
|
-
var oauthCodeTokenRequest =
|
|
4806
|
-
grant_type:
|
|
5379
|
+
var oauthCodeTokenRequest = import_zod20.z.object({
|
|
5380
|
+
grant_type: import_zod20.z.literal("authorization_code").meta({
|
|
4807
5381
|
description: "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
|
|
4808
5382
|
}),
|
|
4809
|
-
code:
|
|
5383
|
+
code: import_zod20.z.string().min(1).max(500).meta({
|
|
4810
5384
|
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."
|
|
4811
5385
|
}),
|
|
4812
5386
|
redirect_uri: redirectUri.meta({
|
|
4813
5387
|
description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
|
|
4814
5388
|
}),
|
|
4815
|
-
client_id:
|
|
5389
|
+
client_id: import_zod20.z.string().min(1).max(200).meta({
|
|
4816
5390
|
description: "The client making the exchange, as registered."
|
|
4817
5391
|
}),
|
|
4818
|
-
code_verifier:
|
|
5392
|
+
code_verifier: import_zod20.z.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
|
|
4819
5393
|
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."
|
|
4820
5394
|
}),
|
|
4821
|
-
resource:
|
|
5395
|
+
resource: import_zod20.z.url().optional().meta({
|
|
4822
5396
|
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."
|
|
4823
5397
|
})
|
|
4824
5398
|
}).meta({
|
|
4825
5399
|
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."
|
|
4826
5400
|
});
|
|
4827
|
-
var oauthRefreshTokenRequest =
|
|
4828
|
-
grant_type:
|
|
5401
|
+
var oauthRefreshTokenRequest = import_zod20.z.object({
|
|
5402
|
+
grant_type: import_zod20.z.literal("refresh_token").meta({
|
|
4829
5403
|
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."
|
|
4830
5404
|
}),
|
|
4831
|
-
refresh_token:
|
|
5405
|
+
refresh_token: import_zod20.z.string().min(1).max(500).meta({
|
|
4832
5406
|
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."
|
|
4833
5407
|
}),
|
|
4834
|
-
client_id:
|
|
5408
|
+
client_id: import_zod20.z.string().min(1).max(200).meta({
|
|
4835
5409
|
description: "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
|
|
4836
5410
|
}),
|
|
4837
|
-
resource:
|
|
5411
|
+
resource: import_zod20.z.url().optional().meta({
|
|
4838
5412
|
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."
|
|
4839
5413
|
})
|
|
4840
5414
|
}).meta({
|
|
4841
5415
|
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."
|
|
4842
5416
|
});
|
|
4843
|
-
var oauthTokenRequest =
|
|
5417
|
+
var oauthTokenRequest = import_zod20.z.discriminatedUnion("grant_type", [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
|
|
4844
5418
|
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."
|
|
4845
5419
|
});
|
|
4846
|
-
var oauthTokenResponse =
|
|
4847
|
-
access_token:
|
|
5420
|
+
var oauthTokenResponse = import_zod20.z.object({
|
|
5421
|
+
access_token: import_zod20.z.string().min(1).meta({
|
|
4848
5422
|
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."
|
|
4849
5423
|
}),
|
|
4850
|
-
token_type:
|
|
5424
|
+
token_type: import_zod20.z.literal("Bearer").meta({
|
|
4851
5425
|
description: "`Bearer`. RFC 6749 \xA75.1 makes the value case-insensitive for a client reading it; this is the spelling this server emits."
|
|
4852
5426
|
}),
|
|
4853
|
-
expires_in:
|
|
5427
|
+
expires_in: import_zod20.z.number().int().positive().meta({
|
|
4854
5428
|
description: "How long the access token is valid, in **seconds**, per RFC 6749 \xA75.1. Not a timestamp, and not milliseconds."
|
|
4855
5429
|
}),
|
|
4856
|
-
refresh_token:
|
|
5430
|
+
refresh_token: import_zod20.z.string().min(1).optional().meta({
|
|
4857
5431
|
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."
|
|
4858
5432
|
}),
|
|
4859
|
-
scope:
|
|
5433
|
+
scope: import_zod20.z.string().max(500).optional().meta({
|
|
4860
5434
|
description: "The scopes the issued token actually carries, space-separated."
|
|
4861
5435
|
})
|
|
4862
5436
|
});
|
|
4863
|
-
var authorizationServerMetadata =
|
|
4864
|
-
issuer:
|
|
5437
|
+
var authorizationServerMetadata = import_zod20.z.object({
|
|
5438
|
+
issuer: import_zod20.z.url().meta({
|
|
4865
5439
|
description: "The issuer identifier of this authorization server, per RFC 8414 \xA72. It is what a client checks a token's `iss` against."
|
|
4866
5440
|
}),
|
|
4867
|
-
authorization_endpoint:
|
|
5441
|
+
authorization_endpoint: import_zod20.z.url().meta({
|
|
4868
5442
|
description: "Where a client sends the user to authorize."
|
|
4869
5443
|
}),
|
|
4870
|
-
token_endpoint:
|
|
5444
|
+
token_endpoint: import_zod20.z.url().meta({
|
|
4871
5445
|
description: "The URL where a client exchanges an authorization code, or a refresh token, for tokens."
|
|
4872
5446
|
}),
|
|
4873
|
-
registration_endpoint:
|
|
5447
|
+
registration_endpoint: import_zod20.z.url().optional().meta({
|
|
4874
5448
|
description: "The URL where a client may register itself, per RFC 7591. Absent when the app does not accept dynamic clients."
|
|
4875
5449
|
}),
|
|
4876
|
-
response_types_supported:
|
|
5450
|
+
response_types_supported: import_zod20.z.array(import_zod20.z.literal("code")).meta({
|
|
4877
5451
|
description: "The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1."
|
|
4878
5452
|
}),
|
|
4879
|
-
grant_types_supported:
|
|
5453
|
+
grant_types_supported: import_zod20.z.array(import_zod20.z.enum(["authorization_code", "refresh_token"])).meta({
|
|
4880
5454
|
description: "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
|
|
4881
5455
|
}),
|
|
4882
|
-
code_challenge_methods_supported:
|
|
5456
|
+
code_challenge_methods_supported: import_zod20.z.array(codeChallengeMethod).meta({
|
|
4883
5457
|
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."
|
|
4884
5458
|
}),
|
|
4885
|
-
token_endpoint_auth_methods_supported:
|
|
5459
|
+
token_endpoint_auth_methods_supported: import_zod20.z.array(import_zod20.z.literal("none")).meta({
|
|
4886
5460
|
description: "How a client authenticates at the token endpoint: `none`, the public-client method, with PKCE protecting the exchange."
|
|
4887
5461
|
}),
|
|
4888
|
-
scopes_supported:
|
|
5462
|
+
scopes_supported: import_zod20.z.array(import_zod20.z.string()).optional().meta({
|
|
4889
5463
|
description: "The scopes this server knows about, where it publishes a list."
|
|
4890
5464
|
})
|
|
4891
5465
|
});
|
|
4892
|
-
var protectedResourceMetadata =
|
|
4893
|
-
resource:
|
|
5466
|
+
var protectedResourceMetadata = import_zod20.z.object({
|
|
5467
|
+
resource: import_zod20.z.url().meta({
|
|
4894
5468
|
description: "The resource identifier this document describes, per RFC 9728. A token whose audience names something else is rejected here rather than merely noted."
|
|
4895
5469
|
}),
|
|
4896
|
-
authorization_servers:
|
|
5470
|
+
authorization_servers: import_zod20.z.array(import_zod20.z.url()).min(1).meta({
|
|
4897
5471
|
description: "The authorization servers that may issue tokens for this resource. There is always at least one."
|
|
4898
5472
|
}),
|
|
4899
|
-
bearer_methods_supported:
|
|
5473
|
+
bearer_methods_supported: import_zod20.z.array(import_zod20.z.literal("header")).meta({
|
|
4900
5474
|
description: "How a token may be presented: in the `Authorization` header only, never in a query parameter or a form field."
|
|
4901
5475
|
}),
|
|
4902
|
-
scopes_supported:
|
|
5476
|
+
scopes_supported: import_zod20.z.array(import_zod20.z.string()).optional().meta({
|
|
4903
5477
|
description: "The scopes this resource understands, where it publishes a list."
|
|
4904
5478
|
})
|
|
4905
5479
|
});
|
|
4906
|
-
var oauthRedirectResponse =
|
|
4907
|
-
redirect_to:
|
|
5480
|
+
var oauthRedirectResponse = import_zod20.z.object({
|
|
5481
|
+
redirect_to: import_zod20.z.string().min(1).max(2e3)
|
|
4908
5482
|
});
|
|
4909
|
-
var oauthAuthorizeQuery =
|
|
4910
|
-
response_type:
|
|
5483
|
+
var oauthAuthorizeQuery = import_zod20.z.object({
|
|
5484
|
+
response_type: import_zod20.z.literal("code").meta({
|
|
4911
5485
|
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`."
|
|
4912
5486
|
}),
|
|
4913
|
-
client_id:
|
|
5487
|
+
client_id: import_zod20.z.string().min(1).meta({
|
|
4914
5488
|
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."
|
|
4915
5489
|
}),
|
|
4916
|
-
redirect_uri:
|
|
5490
|
+
redirect_uri: import_zod20.z.string().min(1).meta({
|
|
4917
5491
|
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."
|
|
4918
5492
|
}),
|
|
4919
|
-
code_challenge:
|
|
5493
|
+
code_challenge: import_zod20.z.string().min(1).meta({
|
|
4920
5494
|
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."
|
|
4921
5495
|
}),
|
|
4922
|
-
code_challenge_method:
|
|
5496
|
+
code_challenge_method: import_zod20.z.literal("S256").meta({
|
|
4923
5497
|
description: "Only `S256`. `plain` is refused: a challenge equal to its verifier defends against nothing."
|
|
4924
5498
|
}),
|
|
4925
|
-
state:
|
|
5499
|
+
state: import_zod20.z.string().optional().meta({
|
|
4926
5500
|
description: "Returned unchanged on the callback, and on the error redirect too, so a client can bind either answer to its own request."
|
|
4927
5501
|
}),
|
|
4928
|
-
resource:
|
|
5502
|
+
resource: import_zod20.z.string().optional().meta({
|
|
4929
5503
|
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."
|
|
4930
5504
|
})
|
|
4931
5505
|
// **No `scope`, because this authorization server issues none.** The field
|
|
@@ -4938,7 +5512,7 @@ var oauthAuthorizeQuery = import_zod18.z.object({
|
|
|
4938
5512
|
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."
|
|
4939
5513
|
});
|
|
4940
5514
|
|
|
4941
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
5515
|
+
// node_modules/.pnpm/@fleetless+contracts@6.2.0-next.2/node_modules/@fleetless/contracts/dist/routes.js
|
|
4942
5516
|
var MCP_APP = MCP_APP_PATHS(":appIdentifier");
|
|
4943
5517
|
var APP_IDENTIFIER = {
|
|
4944
5518
|
name: "appIdentifier",
|
|
@@ -4955,11 +5529,123 @@ var IN_HANDLER_ROUTES = [
|
|
|
4955
5529
|
// The one route whose bearer is **optional**: it answers the same document
|
|
4956
5530
|
// with or without one, and only `already_granted` moves.
|
|
4957
5531
|
"GET /api/client/mcp/interactions/:id",
|
|
5532
|
+
// Two-factor setup: during sign-in the challenge in the body is the
|
|
5533
|
+
// credential, from the app's account settings the app user's bearer is.
|
|
5534
|
+
// Either one, decided in the handler.
|
|
5535
|
+
"POST /api/client/two-factor/setup",
|
|
5536
|
+
"POST /api/client/two-factor/setup/confirm",
|
|
4958
5537
|
"GET /api/asset-links/missing",
|
|
4959
5538
|
"GET /api/asset-links/:token"
|
|
4960
5539
|
];
|
|
4961
5540
|
var DEVELOPER_GUARD = ["unauthorized", "token_expired", "token_revoked"];
|
|
4962
5541
|
var CLIENT_GUARD = ["unauthorized", "token_expired", "token_revoked", "forbidden"];
|
|
5542
|
+
function developerSignInRoutes(prefix) {
|
|
5543
|
+
const section = prefix === "/console/oauth" ? "developer-auth" : "mcp";
|
|
5544
|
+
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";
|
|
5545
|
+
const page = (path, summary, notes) => ({
|
|
5546
|
+
method: "GET",
|
|
5547
|
+
path: `${prefix}${path}`,
|
|
5548
|
+
section,
|
|
5549
|
+
summary,
|
|
5550
|
+
audience: "internal",
|
|
5551
|
+
auth: "none",
|
|
5552
|
+
rateLimited: false,
|
|
5553
|
+
ownerTier: false,
|
|
5554
|
+
status: 200,
|
|
5555
|
+
params: [{ name: "id", description: "The interaction id of this sign-in; the step before redirects the browser here." }],
|
|
5556
|
+
query: null,
|
|
5557
|
+
request: null,
|
|
5558
|
+
response: null,
|
|
5559
|
+
errors: [],
|
|
5560
|
+
transport: "http",
|
|
5561
|
+
notes
|
|
5562
|
+
});
|
|
5563
|
+
const step = (path, summary, response, errors, notes) => ({
|
|
5564
|
+
method: "POST",
|
|
5565
|
+
path: `${prefix}${path}`,
|
|
5566
|
+
section,
|
|
5567
|
+
summary,
|
|
5568
|
+
audience: "internal",
|
|
5569
|
+
auth: "none",
|
|
5570
|
+
rateLimited: true,
|
|
5571
|
+
ownerTier: false,
|
|
5572
|
+
status: 200,
|
|
5573
|
+
params: [],
|
|
5574
|
+
query: null,
|
|
5575
|
+
request: null,
|
|
5576
|
+
response,
|
|
5577
|
+
errors: ["rate_limited", ...errors],
|
|
5578
|
+
transport: "http",
|
|
5579
|
+
notes
|
|
5580
|
+
});
|
|
5581
|
+
return [
|
|
5582
|
+
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\`.`),
|
|
5583
|
+
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."),
|
|
5584
|
+
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."),
|
|
5585
|
+
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."),
|
|
5586
|
+
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\`.`),
|
|
5587
|
+
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."),
|
|
5588
|
+
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\`.`),
|
|
5589
|
+
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."),
|
|
5590
|
+
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."),
|
|
5591
|
+
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`."),
|
|
5592
|
+
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."),
|
|
5593
|
+
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`."),
|
|
5594
|
+
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\`.`)
|
|
5595
|
+
];
|
|
5596
|
+
}
|
|
5597
|
+
var HOSTED_TOKEN = {
|
|
5598
|
+
name: "token",
|
|
5599
|
+
description: "The opaque token from the mailed link; it is never sent as a query parameter."
|
|
5600
|
+
};
|
|
5601
|
+
var HOSTED_INTERACTION = {
|
|
5602
|
+
name: "interaction",
|
|
5603
|
+
description: "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
5604
|
+
};
|
|
5605
|
+
function hostedPage(method, path, summary, notes, params = []) {
|
|
5606
|
+
return {
|
|
5607
|
+
method,
|
|
5608
|
+
path: `/app/:appIdentifier${path}`,
|
|
5609
|
+
section: "client-auth",
|
|
5610
|
+
summary,
|
|
5611
|
+
audience: "internal",
|
|
5612
|
+
auth: "none",
|
|
5613
|
+
rateLimited: method === "POST",
|
|
5614
|
+
ownerTier: false,
|
|
5615
|
+
status: 200,
|
|
5616
|
+
params: [APP_IDENTIFIER, ...params],
|
|
5617
|
+
query: null,
|
|
5618
|
+
request: null,
|
|
5619
|
+
response: null,
|
|
5620
|
+
errors: method === "POST" ? ["rate_limited"] : [],
|
|
5621
|
+
transport: "http",
|
|
5622
|
+
notes
|
|
5623
|
+
};
|
|
5624
|
+
}
|
|
5625
|
+
var HOSTED_FORM = "Renders a form; only its `POST` spends the token, so a mail scanner opening the link changes nothing. ";
|
|
5626
|
+
var HOSTED_DEAD = "A spent, expired or unknown token renders the `410` page with the next step for its kind.";
|
|
5627
|
+
var HOSTED_POST = "HTML: the next page on success, the same page with the problem named on a refusal. ";
|
|
5628
|
+
var HOSTED_APP_ROUTES = [
|
|
5629
|
+
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."),
|
|
5630
|
+
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]),
|
|
5631
|
+
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]),
|
|
5632
|
+
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]),
|
|
5633
|
+
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]),
|
|
5634
|
+
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]),
|
|
5635
|
+
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."),
|
|
5636
|
+
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."),
|
|
5637
|
+
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`."),
|
|
5638
|
+
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]),
|
|
5639
|
+
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."),
|
|
5640
|
+
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]),
|
|
5641
|
+
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."),
|
|
5642
|
+
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."),
|
|
5643
|
+
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.'),
|
|
5644
|
+
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.'),
|
|
5645
|
+
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.'),
|
|
5646
|
+
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]),
|
|
5647
|
+
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.")
|
|
5648
|
+
];
|
|
4963
5649
|
var ROUTES = [
|
|
4964
5650
|
/* ------------------------------------------------------------- health */
|
|
4965
5651
|
{
|
|
@@ -4981,24 +5667,6 @@ var ROUTES = [
|
|
|
4981
5667
|
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."
|
|
4982
5668
|
},
|
|
4983
5669
|
/* ----------------------------------------------------- developer auth */
|
|
4984
|
-
{
|
|
4985
|
-
method: "POST",
|
|
4986
|
-
path: "/api/auth/signup",
|
|
4987
|
-
section: "developer-auth",
|
|
4988
|
-
summary: "Creates an org and its founding Owner, and answers a developer session.",
|
|
4989
|
-
audience: "developer",
|
|
4990
|
-
auth: "none",
|
|
4991
|
-
rateLimited: true,
|
|
4992
|
-
ownerTier: false,
|
|
4993
|
-
status: 201,
|
|
4994
|
-
params: [],
|
|
4995
|
-
query: null,
|
|
4996
|
-
request: signUpRequest,
|
|
4997
|
-
response: signUpResponse,
|
|
4998
|
-
errors: ["rate_limited", "signup_closed", "validation_error", "email_taken"],
|
|
4999
|
-
transport: "http",
|
|
5000
|
-
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."
|
|
5001
|
-
},
|
|
5002
5670
|
{
|
|
5003
5671
|
method: "POST",
|
|
5004
5672
|
path: "/api/auth/refresh",
|
|
@@ -5015,7 +5683,7 @@ var ROUTES = [
|
|
|
5015
5683
|
response: sessionTokens,
|
|
5016
5684
|
errors: ["rate_limited", "validation_error", "token_expired", "token_revoked"],
|
|
5017
5685
|
transport: "http",
|
|
5018
|
-
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
|
|
5686
|
+
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."
|
|
5019
5687
|
},
|
|
5020
5688
|
{
|
|
5021
5689
|
method: "POST",
|
|
@@ -5070,11 +5738,12 @@ var ROUTES = [
|
|
|
5070
5738
|
transport: "http",
|
|
5071
5739
|
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."
|
|
5072
5740
|
},
|
|
5741
|
+
/* ------------------------------------ a developer's own second factors */
|
|
5073
5742
|
{
|
|
5074
|
-
method: "
|
|
5075
|
-
path: "/api/auth/
|
|
5743
|
+
method: "GET",
|
|
5744
|
+
path: "/api/auth/two-factor",
|
|
5076
5745
|
section: "developer-auth",
|
|
5077
|
-
summary: "
|
|
5746
|
+
summary: "Answers the calling developer's passkeys, authenticator, recovery codes left and the org's policy.",
|
|
5078
5747
|
audience: "developer",
|
|
5079
5748
|
auth: "developer",
|
|
5080
5749
|
rateLimited: false,
|
|
@@ -5082,102 +5751,173 @@ var ROUTES = [
|
|
|
5082
5751
|
status: 200,
|
|
5083
5752
|
params: [],
|
|
5084
5753
|
query: null,
|
|
5085
|
-
request:
|
|
5086
|
-
response:
|
|
5087
|
-
errors: [...DEVELOPER_GUARD
|
|
5754
|
+
request: null,
|
|
5755
|
+
response: developerTwoFactor,
|
|
5756
|
+
errors: [...DEVELOPER_GUARD],
|
|
5088
5757
|
transport: "http",
|
|
5089
|
-
notes: "
|
|
5758
|
+
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."
|
|
5090
5759
|
},
|
|
5091
5760
|
{
|
|
5092
5761
|
method: "POST",
|
|
5093
|
-
path: "/api/auth/
|
|
5762
|
+
path: "/api/auth/passkeys/options",
|
|
5094
5763
|
section: "developer-auth",
|
|
5095
|
-
summary: "
|
|
5764
|
+
summary: "Answers the WebAuthn creation options for registering a passkey.",
|
|
5096
5765
|
audience: "developer",
|
|
5097
|
-
auth: "
|
|
5098
|
-
rateLimited:
|
|
5766
|
+
auth: "developer",
|
|
5767
|
+
rateLimited: false,
|
|
5099
5768
|
ownerTier: false,
|
|
5100
|
-
status:
|
|
5769
|
+
status: 200,
|
|
5101
5770
|
params: [],
|
|
5102
5771
|
query: null,
|
|
5103
|
-
request:
|
|
5104
|
-
response:
|
|
5105
|
-
errors: [
|
|
5772
|
+
request: null,
|
|
5773
|
+
response: webauthnOptionsResponse,
|
|
5774
|
+
errors: [...DEVELOPER_GUARD],
|
|
5106
5775
|
transport: "http",
|
|
5107
|
-
notes:
|
|
5776
|
+
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."
|
|
5108
5777
|
},
|
|
5109
|
-
/* ------------------------------------------- client auth (portal pages) */
|
|
5110
5778
|
{
|
|
5111
|
-
method: "
|
|
5112
|
-
path: "/
|
|
5113
|
-
section: "
|
|
5114
|
-
summary:
|
|
5115
|
-
audience: "
|
|
5116
|
-
auth: "
|
|
5779
|
+
method: "POST",
|
|
5780
|
+
path: "/api/auth/passkeys",
|
|
5781
|
+
section: "developer-auth",
|
|
5782
|
+
summary: "Registers a passkey from the browser's answer to the creation options.",
|
|
5783
|
+
audience: "developer",
|
|
5784
|
+
auth: "developer",
|
|
5117
5785
|
rateLimited: false,
|
|
5118
5786
|
ownerTier: false,
|
|
5119
|
-
status:
|
|
5787
|
+
status: 201,
|
|
5120
5788
|
params: [],
|
|
5121
5789
|
query: null,
|
|
5122
|
-
request:
|
|
5123
|
-
response:
|
|
5124
|
-
errors: [],
|
|
5790
|
+
request: createPasskeyRequest,
|
|
5791
|
+
response: createPasskeyResponse,
|
|
5792
|
+
errors: [...DEVELOPER_GUARD, "validation_error"],
|
|
5125
5793
|
transport: "http",
|
|
5126
|
-
notes:
|
|
5794
|
+
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`."
|
|
5127
5795
|
},
|
|
5128
5796
|
{
|
|
5129
|
-
method: "
|
|
5130
|
-
path: "/
|
|
5131
|
-
section: "
|
|
5132
|
-
summary:
|
|
5133
|
-
audience: "
|
|
5134
|
-
auth: "
|
|
5797
|
+
method: "PATCH",
|
|
5798
|
+
path: "/api/auth/passkeys/:id",
|
|
5799
|
+
section: "developer-auth",
|
|
5800
|
+
summary: "Renames one of the caller's passkeys.",
|
|
5801
|
+
audience: "developer",
|
|
5802
|
+
auth: "developer",
|
|
5135
5803
|
rateLimited: false,
|
|
5136
5804
|
ownerTier: false,
|
|
5137
5805
|
status: 200,
|
|
5138
|
-
params: [{ name: "
|
|
5806
|
+
params: [{ name: "id", description: "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`." }],
|
|
5807
|
+
query: null,
|
|
5808
|
+
request: renamePasskeyRequest,
|
|
5809
|
+
response: developerPasskey,
|
|
5810
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5811
|
+
transport: "http"
|
|
5812
|
+
},
|
|
5813
|
+
{
|
|
5814
|
+
method: "DELETE",
|
|
5815
|
+
path: "/api/auth/passkeys/:id",
|
|
5816
|
+
section: "developer-auth",
|
|
5817
|
+
summary: "Removes one of the caller's passkeys.",
|
|
5818
|
+
audience: "developer",
|
|
5819
|
+
auth: "developer",
|
|
5820
|
+
rateLimited: false,
|
|
5821
|
+
ownerTier: false,
|
|
5822
|
+
status: 204,
|
|
5823
|
+
params: [{ name: "id", description: "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`." }],
|
|
5139
5824
|
query: null,
|
|
5140
5825
|
request: null,
|
|
5141
5826
|
response: null,
|
|
5142
|
-
errors: [],
|
|
5827
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict"],
|
|
5143
5828
|
transport: "http",
|
|
5144
|
-
notes:
|
|
5829
|
+
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`."
|
|
5145
5830
|
},
|
|
5146
5831
|
{
|
|
5147
|
-
method: "
|
|
5148
|
-
path: "/
|
|
5149
|
-
section: "
|
|
5150
|
-
summary: "
|
|
5151
|
-
audience: "
|
|
5152
|
-
auth: "
|
|
5832
|
+
method: "POST",
|
|
5833
|
+
path: "/api/auth/totp",
|
|
5834
|
+
section: "developer-auth",
|
|
5835
|
+
summary: "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
5836
|
+
audience: "developer",
|
|
5837
|
+
auth: "developer",
|
|
5153
5838
|
rateLimited: false,
|
|
5154
5839
|
ownerTier: false,
|
|
5155
5840
|
status: 200,
|
|
5156
5841
|
params: [],
|
|
5157
5842
|
query: null,
|
|
5158
5843
|
request: null,
|
|
5159
|
-
response:
|
|
5160
|
-
errors: [],
|
|
5844
|
+
response: twoFactorSetupResponse,
|
|
5845
|
+
errors: [...DEVELOPER_GUARD],
|
|
5161
5846
|
transport: "http",
|
|
5162
|
-
notes: "
|
|
5847
|
+
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."
|
|
5163
5848
|
},
|
|
5164
5849
|
{
|
|
5165
5850
|
method: "POST",
|
|
5166
|
-
path: "/api/auth/
|
|
5851
|
+
path: "/api/auth/totp/confirm",
|
|
5167
5852
|
section: "developer-auth",
|
|
5168
|
-
summary: "
|
|
5853
|
+
summary: "Confirms the pending authenticator with a code it shows now.",
|
|
5169
5854
|
audience: "developer",
|
|
5170
|
-
auth: "
|
|
5855
|
+
auth: "developer",
|
|
5171
5856
|
rateLimited: true,
|
|
5172
5857
|
ownerTier: false,
|
|
5173
|
-
status:
|
|
5858
|
+
status: 200,
|
|
5859
|
+
params: [],
|
|
5860
|
+
query: null,
|
|
5861
|
+
request: totpConfirmRequest,
|
|
5862
|
+
response: totpConfirmResponse,
|
|
5863
|
+
errors: [...DEVELOPER_GUARD, "rate_limited", "validation_error", "invalid_code", "token_spent"],
|
|
5864
|
+
transport: "http",
|
|
5865
|
+
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`."
|
|
5866
|
+
},
|
|
5867
|
+
{
|
|
5868
|
+
method: "DELETE",
|
|
5869
|
+
path: "/api/auth/totp",
|
|
5870
|
+
section: "developer-auth",
|
|
5871
|
+
summary: "Removes the caller's authenticator app.",
|
|
5872
|
+
audience: "developer",
|
|
5873
|
+
auth: "developer",
|
|
5874
|
+
rateLimited: false,
|
|
5875
|
+
ownerTier: false,
|
|
5876
|
+
status: 204,
|
|
5174
5877
|
params: [],
|
|
5175
5878
|
query: null,
|
|
5176
|
-
request:
|
|
5879
|
+
request: null,
|
|
5177
5880
|
response: null,
|
|
5178
|
-
errors: [
|
|
5881
|
+
errors: [...DEVELOPER_GUARD, "not_found", "target_state_conflict"],
|
|
5179
5882
|
transport: "http",
|
|
5180
|
-
notes: "
|
|
5883
|
+
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`."
|
|
5884
|
+
},
|
|
5885
|
+
{
|
|
5886
|
+
method: "POST",
|
|
5887
|
+
path: "/api/auth/recovery-codes",
|
|
5888
|
+
section: "developer-auth",
|
|
5889
|
+
summary: "Issues ten new recovery codes and voids the old ones.",
|
|
5890
|
+
audience: "developer",
|
|
5891
|
+
auth: "developer",
|
|
5892
|
+
rateLimited: false,
|
|
5893
|
+
ownerTier: false,
|
|
5894
|
+
status: 200,
|
|
5895
|
+
params: [],
|
|
5896
|
+
query: null,
|
|
5897
|
+
request: null,
|
|
5898
|
+
response: recoveryCodesResponse,
|
|
5899
|
+
errors: [...DEVELOPER_GUARD, "target_state_conflict"],
|
|
5900
|
+
transport: "http",
|
|
5901
|
+
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`."
|
|
5902
|
+
},
|
|
5903
|
+
/* ------------------------------------------- client auth (portal pages) */
|
|
5904
|
+
{
|
|
5905
|
+
method: "GET",
|
|
5906
|
+
path: "/favicon.svg",
|
|
5907
|
+
section: "client-auth",
|
|
5908
|
+
summary: "Serves the Fleetless icon for the auth portal's and the MCP welcome page's browser tab.",
|
|
5909
|
+
audience: "internal",
|
|
5910
|
+
auth: "none",
|
|
5911
|
+
rateLimited: false,
|
|
5912
|
+
ownerTier: false,
|
|
5913
|
+
status: 200,
|
|
5914
|
+
params: [],
|
|
5915
|
+
query: null,
|
|
5916
|
+
request: null,
|
|
5917
|
+
response: null,
|
|
5918
|
+
errors: [],
|
|
5919
|
+
transport: "http",
|
|
5920
|
+
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."
|
|
5181
5921
|
},
|
|
5182
5922
|
{
|
|
5183
5923
|
method: "POST",
|
|
@@ -5360,7 +6100,7 @@ var ROUTES = [
|
|
|
5360
6100
|
response: role,
|
|
5361
6101
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error"],
|
|
5362
6102
|
transport: "http",
|
|
5363
|
-
notes: 'The body is `{ "name": string }` \u2014 non-empty, trimmed, at most
|
|
6103
|
+
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.'
|
|
5364
6104
|
},
|
|
5365
6105
|
{
|
|
5366
6106
|
method: "GET",
|
|
@@ -5442,6 +6182,48 @@ var ROUTES = [
|
|
|
5442
6182
|
transport: "http",
|
|
5443
6183
|
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."
|
|
5444
6184
|
},
|
|
6185
|
+
{
|
|
6186
|
+
method: "PATCH",
|
|
6187
|
+
path: "/api/apps/:id/roles/:roleId",
|
|
6188
|
+
section: "apps",
|
|
6189
|
+
summary: "Renames a role; its users keep it.",
|
|
6190
|
+
audience: "developer",
|
|
6191
|
+
auth: "developer",
|
|
6192
|
+
rateLimited: false,
|
|
6193
|
+
ownerTier: false,
|
|
6194
|
+
status: 200,
|
|
6195
|
+
params: [
|
|
6196
|
+
{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." },
|
|
6197
|
+
{ name: "roleId", description: "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`." }
|
|
6198
|
+
],
|
|
6199
|
+
query: null,
|
|
6200
|
+
request: roleRenameRequest,
|
|
6201
|
+
response: role,
|
|
6202
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error", "role_name_taken"],
|
|
6203
|
+
transport: "http",
|
|
6204
|
+
notes: "Names are unique per app, compared exactly as stored after trimming. Built-in roles can be renamed."
|
|
6205
|
+
},
|
|
6206
|
+
{
|
|
6207
|
+
method: "DELETE",
|
|
6208
|
+
path: "/api/apps/:id/roles/:roleId",
|
|
6209
|
+
section: "apps",
|
|
6210
|
+
summary: "Deletes a role, moving its users, pending invitations and default-role status to another role.",
|
|
6211
|
+
audience: "developer",
|
|
6212
|
+
auth: "developer",
|
|
6213
|
+
rateLimited: false,
|
|
6214
|
+
ownerTier: false,
|
|
6215
|
+
status: 204,
|
|
6216
|
+
params: [
|
|
6217
|
+
{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." },
|
|
6218
|
+
{ name: "roleId", description: "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`." }
|
|
6219
|
+
],
|
|
6220
|
+
query: roleDeleteQuery,
|
|
6221
|
+
request: null,
|
|
6222
|
+
response: null,
|
|
6223
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error", "role_in_use", "last_role"],
|
|
6224
|
+
transport: "http",
|
|
6225
|
+
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."
|
|
6226
|
+
},
|
|
5445
6227
|
{
|
|
5446
6228
|
method: "POST",
|
|
5447
6229
|
path: "/api/apps/:id/server-keys",
|
|
@@ -5625,9 +6407,27 @@ var ROUTES = [
|
|
|
5625
6407
|
query: null,
|
|
5626
6408
|
request: null,
|
|
5627
6409
|
response: mailOutcome,
|
|
5628
|
-
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict"],
|
|
6410
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict", "method_not_allowed"],
|
|
5629
6411
|
transport: "http",
|
|
5630
|
-
notes: "The support door beside `POST /api/client/password/reset
|
|
6412
|
+
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."
|
|
6413
|
+
},
|
|
6414
|
+
{
|
|
6415
|
+
method: "DELETE",
|
|
6416
|
+
path: "/api/apps/:id/users/:userId/two-factor",
|
|
6417
|
+
section: "apps",
|
|
6418
|
+
summary: "Removes an app user's authenticator and recovery codes and ends every session they hold.",
|
|
6419
|
+
audience: "developer",
|
|
6420
|
+
auth: "developer",
|
|
6421
|
+
rateLimited: false,
|
|
6422
|
+
ownerTier: false,
|
|
6423
|
+
status: 204,
|
|
6424
|
+
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`." }],
|
|
6425
|
+
query: null,
|
|
6426
|
+
request: null,
|
|
6427
|
+
response: null,
|
|
6428
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
6429
|
+
transport: "http",
|
|
6430
|
+
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`."
|
|
5631
6431
|
},
|
|
5632
6432
|
/* ---------------------------- the MCP clients one app user has connected */
|
|
5633
6433
|
{
|
|
@@ -5704,7 +6504,7 @@ var ROUTES = [
|
|
|
5704
6504
|
response: appInvitation,
|
|
5705
6505
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "email_taken", "target_state_conflict", "rate_limited"],
|
|
5706
6506
|
transport: "http",
|
|
5707
|
-
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
|
|
6507
|
+
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."
|
|
5708
6508
|
},
|
|
5709
6509
|
{
|
|
5710
6510
|
method: "POST",
|
|
@@ -5838,7 +6638,7 @@ var ROUTES = [
|
|
|
5838
6638
|
method: "GET",
|
|
5839
6639
|
path: "/api/apps/:id/auth-config",
|
|
5840
6640
|
section: "apps",
|
|
5841
|
-
summary: "Reads the app's auth settings:
|
|
6641
|
+
summary: "Reads the app's auth settings: sign-in methods, two-factor, registration, pages, the hosted look and the MCP switch.",
|
|
5842
6642
|
audience: "developer",
|
|
5843
6643
|
auth: "developer",
|
|
5844
6644
|
rateLimited: false,
|
|
@@ -5850,7 +6650,7 @@ var ROUTES = [
|
|
|
5850
6650
|
response: appAuthConfig,
|
|
5851
6651
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5852
6652
|
transport: "http",
|
|
5853
|
-
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."
|
|
6653
|
+
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."
|
|
5854
6654
|
},
|
|
5855
6655
|
{
|
|
5856
6656
|
method: "PUT",
|
|
@@ -5868,13 +6668,31 @@ var ROUTES = [
|
|
|
5868
6668
|
response: appAuthConfig,
|
|
5869
6669
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5870
6670
|
transport: "http",
|
|
5871
|
-
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
|
|
6671
|
+
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."
|
|
6672
|
+
},
|
|
6673
|
+
{
|
|
6674
|
+
method: "PUT",
|
|
6675
|
+
path: "/api/apps/:id/auth-config/sign-in",
|
|
6676
|
+
section: "apps",
|
|
6677
|
+
summary: "Replaces how the app's users sign in and whether they give a second factor.",
|
|
6678
|
+
audience: "developer",
|
|
6679
|
+
auth: "developer",
|
|
6680
|
+
rateLimited: false,
|
|
6681
|
+
ownerTier: false,
|
|
6682
|
+
status: 200,
|
|
6683
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6684
|
+
query: null,
|
|
6685
|
+
request: putAppAuthSignInRequest,
|
|
6686
|
+
response: appAuthConfig,
|
|
6687
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
6688
|
+
transport: "http",
|
|
6689
|
+
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."
|
|
5872
6690
|
},
|
|
5873
6691
|
{
|
|
5874
6692
|
method: "PUT",
|
|
5875
6693
|
path: "/api/apps/:id/auth-config/urls",
|
|
5876
6694
|
section: "apps",
|
|
5877
|
-
summary: "Replaces the
|
|
6695
|
+
summary: "Replaces the app's home page and the four pages Fleetless's mails and MCP sign-in point at.",
|
|
5878
6696
|
audience: "developer",
|
|
5879
6697
|
auth: "developer",
|
|
5880
6698
|
rateLimited: false,
|
|
@@ -5886,13 +6704,13 @@ var ROUTES = [
|
|
|
5886
6704
|
response: appAuthConfig,
|
|
5887
6705
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5888
6706
|
transport: "http",
|
|
5889
|
-
notes: "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `
|
|
6707
|
+
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."
|
|
5890
6708
|
},
|
|
5891
6709
|
{
|
|
5892
6710
|
method: "PUT",
|
|
5893
6711
|
path: "/api/apps/:id/auth-config/mcp",
|
|
5894
6712
|
section: "apps",
|
|
5895
|
-
summary: "
|
|
6713
|
+
summary: "Turns the app's MCP endpoint on or off.",
|
|
5896
6714
|
audience: "developer",
|
|
5897
6715
|
auth: "developer",
|
|
5898
6716
|
rateLimited: false,
|
|
@@ -5904,7 +6722,61 @@ var ROUTES = [
|
|
|
5904
6722
|
response: appAuthConfig,
|
|
5905
6723
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5906
6724
|
transport: "http",
|
|
5907
|
-
notes: "**A replace, not a merge, and `.strict()`**: `mcp_enabled`
|
|
6725
|
+
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."
|
|
6726
|
+
},
|
|
6727
|
+
{
|
|
6728
|
+
method: "PUT",
|
|
6729
|
+
path: "/api/apps/:id/auth-config/look",
|
|
6730
|
+
section: "apps",
|
|
6731
|
+
summary: "Replaces the hosted pages' accent colour.",
|
|
6732
|
+
audience: "developer",
|
|
6733
|
+
auth: "developer",
|
|
6734
|
+
rateLimited: false,
|
|
6735
|
+
ownerTier: false,
|
|
6736
|
+
status: 200,
|
|
6737
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6738
|
+
query: null,
|
|
6739
|
+
request: putAppAuthLookRequest,
|
|
6740
|
+
response: appAuthConfig,
|
|
6741
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
6742
|
+
transport: "http",
|
|
6743
|
+
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."
|
|
6744
|
+
},
|
|
6745
|
+
{
|
|
6746
|
+
method: "PUT",
|
|
6747
|
+
path: "/api/apps/:id/auth-config/logo",
|
|
6748
|
+
section: "apps",
|
|
6749
|
+
summary: "Stores the logo the hosted pages show above the app's name.",
|
|
6750
|
+
audience: "developer",
|
|
6751
|
+
auth: "developer",
|
|
6752
|
+
rateLimited: false,
|
|
6753
|
+
ownerTier: false,
|
|
6754
|
+
status: 200,
|
|
6755
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6756
|
+
query: null,
|
|
6757
|
+
request: null,
|
|
6758
|
+
response: appAuthConfig,
|
|
6759
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "unsupported_media_type"],
|
|
6760
|
+
transport: "http",
|
|
6761
|
+
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."
|
|
6762
|
+
},
|
|
6763
|
+
{
|
|
6764
|
+
method: "DELETE",
|
|
6765
|
+
path: "/api/apps/:id/auth-config/logo",
|
|
6766
|
+
section: "apps",
|
|
6767
|
+
summary: "Removes the logo from the hosted pages.",
|
|
6768
|
+
audience: "developer",
|
|
6769
|
+
auth: "developer",
|
|
6770
|
+
rateLimited: false,
|
|
6771
|
+
ownerTier: false,
|
|
6772
|
+
status: 200,
|
|
6773
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6774
|
+
query: null,
|
|
6775
|
+
request: null,
|
|
6776
|
+
response: appAuthConfig,
|
|
6777
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
6778
|
+
transport: "http",
|
|
6779
|
+
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."
|
|
5908
6780
|
},
|
|
5909
6781
|
{
|
|
5910
6782
|
method: "GET",
|
|
@@ -5922,7 +6794,7 @@ var ROUTES = [
|
|
|
5922
6794
|
response: appMailTemplateListResponse,
|
|
5923
6795
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5924
6796
|
transport: "http",
|
|
5925
|
-
notes: 'Answers `{ "templates": [appMailTemplate, \u2026] }` with **only the kinds that have a custom template** \u2014 at most
|
|
6797
|
+
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.'
|
|
5926
6798
|
},
|
|
5927
6799
|
{
|
|
5928
6800
|
method: "GET",
|
|
@@ -5934,7 +6806,7 @@ var ROUTES = [
|
|
|
5934
6806
|
rateLimited: false,
|
|
5935
6807
|
ownerTier: false,
|
|
5936
6808
|
status: 200,
|
|
5937
|
-
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
|
|
6809
|
+
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`." }],
|
|
5938
6810
|
query: null,
|
|
5939
6811
|
request: null,
|
|
5940
6812
|
response: appMailTemplate,
|
|
@@ -5952,7 +6824,7 @@ var ROUTES = [
|
|
|
5952
6824
|
rateLimited: false,
|
|
5953
6825
|
ownerTier: false,
|
|
5954
6826
|
status: 200,
|
|
5955
|
-
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
|
|
6827
|
+
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`." }],
|
|
5956
6828
|
query: null,
|
|
5957
6829
|
request: putAppMailTemplateRequest,
|
|
5958
6830
|
response: appMailTemplate,
|
|
@@ -5970,7 +6842,7 @@ var ROUTES = [
|
|
|
5970
6842
|
rateLimited: false,
|
|
5971
6843
|
ownerTier: false,
|
|
5972
6844
|
status: 204,
|
|
5973
|
-
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
|
|
6845
|
+
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`." }],
|
|
5974
6846
|
query: null,
|
|
5975
6847
|
request: null,
|
|
5976
6848
|
response: null,
|
|
@@ -5988,7 +6860,7 @@ var ROUTES = [
|
|
|
5988
6860
|
rateLimited: false,
|
|
5989
6861
|
ownerTier: false,
|
|
5990
6862
|
status: 200,
|
|
5991
|
-
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
|
|
6863
|
+
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`." }],
|
|
5992
6864
|
query: null,
|
|
5993
6865
|
request: mailTemplatePreviewRequest,
|
|
5994
6866
|
response: mailTemplatePreviewResponse,
|
|
@@ -6006,7 +6878,7 @@ var ROUTES = [
|
|
|
6006
6878
|
rateLimited: false,
|
|
6007
6879
|
ownerTier: false,
|
|
6008
6880
|
status: 202,
|
|
6009
|
-
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
|
|
6881
|
+
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`." }],
|
|
6010
6882
|
query: null,
|
|
6011
6883
|
request: mailTemplatePreviewRequest,
|
|
6012
6884
|
response: mailOutcome,
|
|
@@ -6126,7 +6998,7 @@ var ROUTES = [
|
|
|
6126
6998
|
method: "POST",
|
|
6127
6999
|
path: "/api/org/invitations/accept",
|
|
6128
7000
|
section: "users",
|
|
6129
|
-
summary: "Spends an invitation token and creates the
|
|
7001
|
+
summary: "Spends an invitation token and creates the account it was addressed to.",
|
|
6130
7002
|
audience: "developer",
|
|
6131
7003
|
auth: "none",
|
|
6132
7004
|
rateLimited: true,
|
|
@@ -6138,7 +7010,7 @@ var ROUTES = [
|
|
|
6138
7010
|
response: null,
|
|
6139
7011
|
errors: ["rate_limited", "validation_error", "token_spent", "email_taken"],
|
|
6140
7012
|
transport: "http",
|
|
6141
|
-
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.'
|
|
7013
|
+
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.'
|
|
6142
7014
|
},
|
|
6143
7015
|
{
|
|
6144
7016
|
method: "GET",
|
|
@@ -6156,7 +7028,7 @@ var ROUTES = [
|
|
|
6156
7028
|
response: null,
|
|
6157
7029
|
errors: [],
|
|
6158
7030
|
transport: "http",
|
|
6159
|
-
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
|
|
7031
|
+
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.'
|
|
6160
7032
|
},
|
|
6161
7033
|
{
|
|
6162
7034
|
method: "PATCH",
|
|
@@ -6212,11 +7084,29 @@ var ROUTES = [
|
|
|
6212
7084
|
transport: "http",
|
|
6213
7085
|
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."
|
|
6214
7086
|
},
|
|
7087
|
+
{
|
|
7088
|
+
method: "DELETE",
|
|
7089
|
+
path: "/api/org/users/:id/two-factor",
|
|
7090
|
+
section: "users",
|
|
7091
|
+
summary: "Removes a team member's passkeys, authenticator and recovery codes and ends their sessions.",
|
|
7092
|
+
audience: "developer",
|
|
7093
|
+
auth: "developer",
|
|
7094
|
+
rateLimited: false,
|
|
7095
|
+
ownerTier: true,
|
|
7096
|
+
status: 204,
|
|
7097
|
+
params: [{ name: "id", description: "The Fleetless user's uuid, as listed by `GET /api/org/users`." }],
|
|
7098
|
+
query: null,
|
|
7099
|
+
request: null,
|
|
7100
|
+
response: null,
|
|
7101
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "invalid_uuid", "not_found", "target_state_conflict"],
|
|
7102
|
+
transport: "http",
|
|
7103
|
+
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."
|
|
7104
|
+
},
|
|
6215
7105
|
{
|
|
6216
7106
|
method: "PATCH",
|
|
6217
7107
|
path: "/api/org",
|
|
6218
7108
|
section: "org",
|
|
6219
|
-
summary: "Renames the org.",
|
|
7109
|
+
summary: "Renames the org, requires two-factor for its members, or both.",
|
|
6220
7110
|
audience: "developer",
|
|
6221
7111
|
auth: "developer",
|
|
6222
7112
|
rateLimited: false,
|
|
@@ -6228,7 +7118,7 @@ var ROUTES = [
|
|
|
6228
7118
|
response: patchOrgResponse,
|
|
6229
7119
|
errors: [...DEVELOPER_GUARD, "tier_required", "validation_error"],
|
|
6230
7120
|
transport: "http",
|
|
6231
|
-
notes: 'Answers `{ "org": org }`. Owner tier, and the gate runs before the body is looked at, so a malformed
|
|
7121
|
+
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`.'
|
|
6232
7122
|
},
|
|
6233
7123
|
/* --------------------------------------------------------------- mcp */
|
|
6234
7124
|
{
|
|
@@ -6301,7 +7191,7 @@ var ROUTES = [
|
|
|
6301
7191
|
response: null,
|
|
6302
7192
|
errors: [],
|
|
6303
7193
|
transport: "http",
|
|
6304
|
-
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
|
|
7194
|
+
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."
|
|
6305
7195
|
},
|
|
6306
7196
|
{
|
|
6307
7197
|
method: "GET",
|
|
@@ -6319,44 +7209,9 @@ var ROUTES = [
|
|
|
6319
7209
|
response: null,
|
|
6320
7210
|
errors: [],
|
|
6321
7211
|
transport: "http",
|
|
6322
|
-
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."
|
|
6323
|
-
},
|
|
6324
|
-
{
|
|
6325
|
-
method: "POST",
|
|
6326
|
-
path: "/mcp/oauth/identify",
|
|
6327
|
-
section: "mcp",
|
|
6328
|
-
summary: "Takes the email address and hands back the password step.",
|
|
6329
|
-
audience: "internal",
|
|
6330
|
-
auth: "none",
|
|
6331
|
-
rateLimited: true,
|
|
6332
|
-
ownerTier: false,
|
|
6333
|
-
status: 200,
|
|
6334
|
-
params: [],
|
|
6335
|
-
query: null,
|
|
6336
|
-
request: null,
|
|
6337
|
-
response: null,
|
|
6338
|
-
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
6339
|
-
transport: "http",
|
|
6340
|
-
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.'
|
|
6341
|
-
},
|
|
6342
|
-
{
|
|
6343
|
-
method: "POST",
|
|
6344
|
-
path: "/mcp/oauth/login",
|
|
6345
|
-
section: "mcp",
|
|
6346
|
-
summary: "Checks the password and hands back where the MCP sign-in continues.",
|
|
6347
|
-
audience: "internal",
|
|
6348
|
-
auth: "none",
|
|
6349
|
-
rateLimited: true,
|
|
6350
|
-
ownerTier: false,
|
|
6351
|
-
status: 200,
|
|
6352
|
-
params: [],
|
|
6353
|
-
query: null,
|
|
6354
|
-
request: null,
|
|
6355
|
-
response: oauthRedirectResponse,
|
|
6356
|
-
errors: ["rate_limited", "validation_error", "token_spent", "invalid_credentials"],
|
|
6357
|
-
transport: "http",
|
|
6358
|
-
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`.'
|
|
7212
|
+
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."
|
|
6359
7213
|
},
|
|
7214
|
+
...developerSignInRoutes("/mcp/oauth"),
|
|
6360
7215
|
{
|
|
6361
7216
|
method: "GET",
|
|
6362
7217
|
path: "/mcp/oauth/consent/:id",
|
|
@@ -6367,7 +7222,7 @@ var ROUTES = [
|
|
|
6367
7222
|
rateLimited: false,
|
|
6368
7223
|
ownerTier: false,
|
|
6369
7224
|
status: 200,
|
|
6370
|
-
params: [{ name: "id", description: "The interaction id from the sign-in; the
|
|
7225
|
+
params: [{ name: "id", description: "The interaction id from the sign-in; the last sign-in step redirects the browser here." }],
|
|
6371
7226
|
query: null,
|
|
6372
7227
|
request: null,
|
|
6373
7228
|
response: null,
|
|
@@ -6446,31 +7301,32 @@ var ROUTES = [
|
|
|
6446
7301
|
response: null,
|
|
6447
7302
|
errors: [],
|
|
6448
7303
|
transport: "http",
|
|
6449
|
-
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."
|
|
7304
|
+
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."
|
|
6450
7305
|
},
|
|
7306
|
+
...developerSignInRoutes("/console/oauth"),
|
|
6451
7307
|
{
|
|
6452
|
-
method: "
|
|
6453
|
-
path: "/console/oauth/
|
|
7308
|
+
method: "GET",
|
|
7309
|
+
path: "/console/oauth/signup/:id",
|
|
6454
7310
|
section: "developer-auth",
|
|
6455
|
-
summary: "
|
|
7311
|
+
summary: "Serves step one of console sign-up, the email card.",
|
|
6456
7312
|
audience: "internal",
|
|
6457
7313
|
auth: "none",
|
|
6458
|
-
rateLimited:
|
|
7314
|
+
rateLimited: false,
|
|
6459
7315
|
ownerTier: false,
|
|
6460
7316
|
status: 200,
|
|
6461
|
-
params: [],
|
|
7317
|
+
params: [{ name: "id", description: "The interaction id minted by `GET /console/oauth/authorize` with `?prompt=create`." }],
|
|
6462
7318
|
query: null,
|
|
6463
7319
|
request: null,
|
|
6464
7320
|
response: null,
|
|
6465
|
-
errors: [
|
|
7321
|
+
errors: [],
|
|
6466
7322
|
transport: "http",
|
|
6467
|
-
notes: '
|
|
7323
|
+
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.'
|
|
6468
7324
|
},
|
|
6469
7325
|
{
|
|
6470
7326
|
method: "POST",
|
|
6471
|
-
path: "/console/oauth/
|
|
7327
|
+
path: "/console/oauth/signup",
|
|
6472
7328
|
section: "developer-auth",
|
|
6473
|
-
summary: "
|
|
7329
|
+
summary: "Takes the sign-up email, mails a code and hands back the code step.",
|
|
6474
7330
|
audience: "internal",
|
|
6475
7331
|
auth: "none",
|
|
6476
7332
|
rateLimited: true,
|
|
@@ -6479,34 +7335,16 @@ var ROUTES = [
|
|
|
6479
7335
|
params: [],
|
|
6480
7336
|
query: null,
|
|
6481
7337
|
request: null,
|
|
6482
|
-
response: oauthRedirectResponse,
|
|
6483
|
-
errors: ["rate_limited", "token_spent", "invalid_credentials"],
|
|
6484
|
-
transport: "http",
|
|
6485
|
-
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`.'
|
|
6486
|
-
},
|
|
6487
|
-
{
|
|
6488
|
-
method: "GET",
|
|
6489
|
-
path: "/console/oauth/signup/:id",
|
|
6490
|
-
section: "developer-auth",
|
|
6491
|
-
summary: "Serves step one of console sign-up, the account card.",
|
|
6492
|
-
audience: "internal",
|
|
6493
|
-
auth: "none",
|
|
6494
|
-
rateLimited: false,
|
|
6495
|
-
ownerTier: false,
|
|
6496
|
-
status: 200,
|
|
6497
|
-
params: [{ name: "id", description: "The interaction id minted by `GET /console/oauth/authorize` with `?prompt=create`." }],
|
|
6498
|
-
query: null,
|
|
6499
|
-
request: null,
|
|
6500
7338
|
response: null,
|
|
6501
|
-
errors: [],
|
|
7339
|
+
errors: ["rate_limited", "token_spent", "signup_closed", "validation_error", "email_taken"],
|
|
6502
7340
|
transport: "http",
|
|
6503
|
-
notes: '
|
|
7341
|
+
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.'
|
|
6504
7342
|
},
|
|
6505
7343
|
{
|
|
6506
7344
|
method: "POST",
|
|
6507
|
-
path: "/console/oauth/signup",
|
|
7345
|
+
path: "/console/oauth/signup/code",
|
|
6508
7346
|
section: "developer-auth",
|
|
6509
|
-
summary: "
|
|
7347
|
+
summary: "Checks the sign-up code and hands back the organization step.",
|
|
6510
7348
|
audience: "internal",
|
|
6511
7349
|
auth: "none",
|
|
6512
7350
|
rateLimited: true,
|
|
@@ -6516,9 +7354,9 @@ var ROUTES = [
|
|
|
6516
7354
|
query: null,
|
|
6517
7355
|
request: null,
|
|
6518
7356
|
response: null,
|
|
6519
|
-
errors: ["rate_limited", "token_spent", "signup_closed", "validation_error", "
|
|
7357
|
+
errors: ["rate_limited", "token_spent", "signup_closed", "wrong_browser", "validation_error", "invalid_code"],
|
|
6520
7358
|
transport: "http",
|
|
6521
|
-
notes: 'A
|
|
7359
|
+
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.'
|
|
6522
7360
|
},
|
|
6523
7361
|
{
|
|
6524
7362
|
method: "POST",
|
|
@@ -6536,7 +7374,7 @@ var ROUTES = [
|
|
|
6536
7374
|
response: oauthRedirectResponse,
|
|
6537
7375
|
errors: ["rate_limited", "token_spent", "signup_closed", "wrong_browser", "validation_error", "email_taken"],
|
|
6538
7376
|
transport: "http",
|
|
6539
|
-
notes: "The
|
|
7377
|
+
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."
|
|
6540
7378
|
},
|
|
6541
7379
|
{
|
|
6542
7380
|
method: "POST",
|
|
@@ -6556,6 +7394,8 @@ var ROUTES = [
|
|
|
6556
7394
|
transport: "http",
|
|
6557
7395
|
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."
|
|
6558
7396
|
},
|
|
7397
|
+
/* --------------------------------- the Fleetless-hosted app pages */
|
|
7398
|
+
...HOSTED_APP_ROUTES,
|
|
6559
7399
|
/* ---------------------------------------------------- mcp (the endpoint) */
|
|
6560
7400
|
{
|
|
6561
7401
|
method: "GET",
|
|
@@ -6682,7 +7522,7 @@ var ROUTES = [
|
|
|
6682
7522
|
response: authorizationServerMetadata,
|
|
6683
7523
|
errors: ["not_found"],
|
|
6684
7524
|
transport: "http",
|
|
6685
|
-
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
|
|
7525
|
+
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."
|
|
6686
7526
|
},
|
|
6687
7527
|
{
|
|
6688
7528
|
method: "POST",
|
|
@@ -6706,7 +7546,7 @@ var ROUTES = [
|
|
|
6706
7546
|
method: "GET",
|
|
6707
7547
|
path: MCP_APP.authorize,
|
|
6708
7548
|
section: "mcp",
|
|
6709
|
-
summary: "Starts an MCP sign-in and redirects the browser to the app's own login page.",
|
|
7549
|
+
summary: "Starts an MCP sign-in and redirects the browser to the app's own login page, or to the hosted one.",
|
|
6710
7550
|
audience: "client",
|
|
6711
7551
|
auth: "none",
|
|
6712
7552
|
rateLimited: false,
|
|
@@ -6716,9 +7556,9 @@ var ROUTES = [
|
|
|
6716
7556
|
query: oauthAuthorizeQuery,
|
|
6717
7557
|
request: null,
|
|
6718
7558
|
response: null,
|
|
6719
|
-
errors: ["not_found"
|
|
7559
|
+
errors: ["not_found"],
|
|
6720
7560
|
transport: "http",
|
|
6721
|
-
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**
|
|
7561
|
+
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`.'
|
|
6722
7562
|
},
|
|
6723
7563
|
{
|
|
6724
7564
|
method: "POST",
|
|
@@ -6752,10 +7592,46 @@ var ROUTES = [
|
|
|
6752
7592
|
params: [],
|
|
6753
7593
|
query: null,
|
|
6754
7594
|
request: clientLoginRequest,
|
|
6755
|
-
response:
|
|
6756
|
-
errors: ["rate_limited", "validation_error", "invalid_credentials"],
|
|
7595
|
+
response: clientSignInResult,
|
|
7596
|
+
errors: ["rate_limited", "validation_error", "invalid_credentials", "method_not_allowed"],
|
|
7597
|
+
transport: "http",
|
|
7598
|
+
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.'
|
|
7599
|
+
},
|
|
7600
|
+
{
|
|
7601
|
+
method: "POST",
|
|
7602
|
+
path: "/api/client/login/code",
|
|
7603
|
+
section: "client-auth",
|
|
7604
|
+
summary: "Mails a six-digit sign-in code, and answers the same whether or not the address exists.",
|
|
7605
|
+
audience: "client",
|
|
7606
|
+
auth: "none",
|
|
7607
|
+
rateLimited: true,
|
|
7608
|
+
ownerTier: false,
|
|
7609
|
+
status: 202,
|
|
7610
|
+
params: [],
|
|
7611
|
+
query: null,
|
|
7612
|
+
request: clientLoginCodeRequest,
|
|
7613
|
+
response: null,
|
|
7614
|
+
errors: ["rate_limited", "validation_error", "not_found", "method_not_allowed"],
|
|
6757
7615
|
transport: "http",
|
|
6758
|
-
notes:
|
|
7616
|
+
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."
|
|
7617
|
+
},
|
|
7618
|
+
{
|
|
7619
|
+
method: "POST",
|
|
7620
|
+
path: "/api/client/login/code/verify",
|
|
7621
|
+
section: "client-auth",
|
|
7622
|
+
summary: "Spends a mailed sign-in code and answers a session or a two-factor challenge.",
|
|
7623
|
+
audience: "client",
|
|
7624
|
+
auth: "none",
|
|
7625
|
+
rateLimited: true,
|
|
7626
|
+
ownerTier: false,
|
|
7627
|
+
status: 200,
|
|
7628
|
+
params: [],
|
|
7629
|
+
query: null,
|
|
7630
|
+
request: clientLoginCodeVerifyRequest,
|
|
7631
|
+
response: clientSignInResult,
|
|
7632
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent", "method_not_allowed"],
|
|
7633
|
+
transport: "http",
|
|
7634
|
+
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."
|
|
6759
7635
|
},
|
|
6760
7636
|
{
|
|
6761
7637
|
method: "POST",
|
|
@@ -6773,7 +7649,7 @@ var ROUTES = [
|
|
|
6773
7649
|
response: null,
|
|
6774
7650
|
errors: ["rate_limited", "validation_error", "not_found", "registration_closed", "domain_not_allowed", "target_state_conflict", "quota_exceeded"],
|
|
6775
7651
|
transport: "http",
|
|
6776
|
-
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
|
|
7652
|
+
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."
|
|
6777
7653
|
},
|
|
6778
7654
|
{
|
|
6779
7655
|
method: "POST",
|
|
@@ -6788,10 +7664,10 @@ var ROUTES = [
|
|
|
6788
7664
|
params: [],
|
|
6789
7665
|
query: null,
|
|
6790
7666
|
request: clientVerifyEmailRequest,
|
|
6791
|
-
response:
|
|
7667
|
+
response: clientSignInResult,
|
|
6792
7668
|
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
6793
7669
|
transport: "http",
|
|
6794
|
-
notes: "**The answer is a session, not a `204
|
|
7670
|
+
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."
|
|
6795
7671
|
},
|
|
6796
7672
|
{
|
|
6797
7673
|
method: "POST",
|
|
@@ -6825,9 +7701,9 @@ var ROUTES = [
|
|
|
6825
7701
|
query: null,
|
|
6826
7702
|
request: clientPasswordResetRequest,
|
|
6827
7703
|
response: null,
|
|
6828
|
-
errors: ["rate_limited", "validation_error", "not_found"],
|
|
7704
|
+
errors: ["rate_limited", "validation_error", "not_found", "method_not_allowed"],
|
|
6829
7705
|
transport: "http",
|
|
6830
|
-
notes: "
|
|
7706
|
+
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."
|
|
6831
7707
|
},
|
|
6832
7708
|
{
|
|
6833
7709
|
method: "POST",
|
|
@@ -6842,10 +7718,10 @@ var ROUTES = [
|
|
|
6842
7718
|
params: [],
|
|
6843
7719
|
query: null,
|
|
6844
7720
|
request: clientPasswordResetConfirmRequest,
|
|
6845
|
-
response:
|
|
6846
|
-
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
7721
|
+
response: clientSignInResult,
|
|
7722
|
+
errors: ["rate_limited", "validation_error", "token_spent", "method_not_allowed"],
|
|
6847
7723
|
transport: "http",
|
|
6848
|
-
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."
|
|
7724
|
+
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."
|
|
6849
7725
|
},
|
|
6850
7726
|
{
|
|
6851
7727
|
method: "POST",
|
|
@@ -6860,10 +7736,10 @@ var ROUTES = [
|
|
|
6860
7736
|
params: [],
|
|
6861
7737
|
query: null,
|
|
6862
7738
|
request: clientAcceptInvitationRequest,
|
|
6863
|
-
response:
|
|
7739
|
+
response: clientSignInResult,
|
|
6864
7740
|
errors: ["rate_limited", "validation_error", "token_spent", "email_taken", "target_state_conflict", "quota_exceeded"],
|
|
6865
7741
|
transport: "http",
|
|
6866
|
-
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."
|
|
7742
|
+
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."
|
|
6867
7743
|
},
|
|
6868
7744
|
{
|
|
6869
7745
|
method: "POST",
|
|
@@ -6915,9 +7791,9 @@ var ROUTES = [
|
|
|
6915
7791
|
query: null,
|
|
6916
7792
|
request: passwordChangeRequest,
|
|
6917
7793
|
response: sessionTokens,
|
|
6918
|
-
errors: [...CLIENT_GUARD, "validation_error", "invalid_credentials", "target_state_conflict"],
|
|
7794
|
+
errors: [...CLIENT_GUARD, "validation_error", "invalid_credentials", "target_state_conflict", "method_not_allowed"],
|
|
6919
7795
|
transport: "http",
|
|
6920
|
-
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.'
|
|
7796
|
+
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.'
|
|
6921
7797
|
},
|
|
6922
7798
|
{
|
|
6923
7799
|
method: "GET",
|
|
@@ -6937,6 +7813,80 @@ var ROUTES = [
|
|
|
6937
7813
|
transport: "http",
|
|
6938
7814
|
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."
|
|
6939
7815
|
},
|
|
7816
|
+
/* ------------------------------------------------ app-user two-factor */
|
|
7817
|
+
{
|
|
7818
|
+
method: "POST",
|
|
7819
|
+
path: "/api/client/two-factor/verify",
|
|
7820
|
+
section: "client-auth",
|
|
7821
|
+
summary: "Answers a two-factor challenge with an authenticator or recovery code, and answers the session.",
|
|
7822
|
+
audience: "client",
|
|
7823
|
+
auth: "none",
|
|
7824
|
+
rateLimited: true,
|
|
7825
|
+
ownerTier: false,
|
|
7826
|
+
status: 200,
|
|
7827
|
+
params: [],
|
|
7828
|
+
query: null,
|
|
7829
|
+
request: clientTwoFactorVerifyRequest,
|
|
7830
|
+
response: sessionTokens,
|
|
7831
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent"],
|
|
7832
|
+
transport: "http",
|
|
7833
|
+
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`."
|
|
7834
|
+
},
|
|
7835
|
+
{
|
|
7836
|
+
method: "POST",
|
|
7837
|
+
path: "/api/client/two-factor/setup",
|
|
7838
|
+
section: "client-auth",
|
|
7839
|
+
summary: "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
7840
|
+
audience: "client",
|
|
7841
|
+
auth: "in_handler",
|
|
7842
|
+
rateLimited: true,
|
|
7843
|
+
ownerTier: false,
|
|
7844
|
+
status: 200,
|
|
7845
|
+
params: [],
|
|
7846
|
+
query: null,
|
|
7847
|
+
request: clientTwoFactorSetupRequest,
|
|
7848
|
+
requestOptional: true,
|
|
7849
|
+
response: twoFactorSetupResponse,
|
|
7850
|
+
errors: ["rate_limited", "validation_error", "token_spent", "unauthorized", "target_state_conflict"],
|
|
7851
|
+
transport: "http",
|
|
7852
|
+
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."
|
|
7853
|
+
},
|
|
7854
|
+
{
|
|
7855
|
+
method: "POST",
|
|
7856
|
+
path: "/api/client/two-factor/setup/confirm",
|
|
7857
|
+
section: "client-auth",
|
|
7858
|
+
summary: "Confirms the new authenticator with a code and answers the recovery codes and a session.",
|
|
7859
|
+
audience: "client",
|
|
7860
|
+
auth: "in_handler",
|
|
7861
|
+
rateLimited: true,
|
|
7862
|
+
ownerTier: false,
|
|
7863
|
+
status: 200,
|
|
7864
|
+
params: [],
|
|
7865
|
+
query: null,
|
|
7866
|
+
request: clientTwoFactorSetupConfirmRequest,
|
|
7867
|
+
response: clientTwoFactorSetupConfirmResponse,
|
|
7868
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent", "unauthorized"],
|
|
7869
|
+
transport: "http",
|
|
7870
|
+
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`."
|
|
7871
|
+
},
|
|
7872
|
+
{
|
|
7873
|
+
method: "DELETE",
|
|
7874
|
+
path: "/api/client/two-factor",
|
|
7875
|
+
section: "client-auth",
|
|
7876
|
+
summary: "Turns the signed-in app user's authenticator off.",
|
|
7877
|
+
audience: "client",
|
|
7878
|
+
auth: "developer_or_client",
|
|
7879
|
+
rateLimited: true,
|
|
7880
|
+
ownerTier: false,
|
|
7881
|
+
status: 204,
|
|
7882
|
+
params: [],
|
|
7883
|
+
query: null,
|
|
7884
|
+
request: clientTwoFactorDisableRequest,
|
|
7885
|
+
response: null,
|
|
7886
|
+
errors: [...CLIENT_GUARD, "rate_limited", "validation_error", "invalid_code", "target_state_conflict"],
|
|
7887
|
+
transport: "http",
|
|
7888
|
+
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`."
|
|
7889
|
+
},
|
|
6940
7890
|
/* ---------------------------------------- app-user sign-in through an IdP */
|
|
6941
7891
|
{
|
|
6942
7892
|
method: "GET",
|
|
@@ -6990,7 +7940,7 @@ var ROUTES = [
|
|
|
6990
7940
|
response: null,
|
|
6991
7941
|
errors: ["rate_limited"],
|
|
6992
7942
|
transport: "http",
|
|
6993
|
-
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.
|
|
7943
|
+
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."
|
|
6994
7944
|
},
|
|
6995
7945
|
{
|
|
6996
7946
|
method: "POST",
|
|
@@ -8181,6 +9131,97 @@ var ROUTES = [
|
|
|
8181
9131
|
transport: "http",
|
|
8182
9132
|
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."
|
|
8183
9133
|
},
|
|
9134
|
+
{
|
|
9135
|
+
method: "POST",
|
|
9136
|
+
path: "/api/feedback",
|
|
9137
|
+
section: "org",
|
|
9138
|
+
summary: "Sends a message from a developer to the people who build Fleetless.",
|
|
9139
|
+
audience: "developer",
|
|
9140
|
+
auth: "developer",
|
|
9141
|
+
rateLimited: true,
|
|
9142
|
+
ownerTier: false,
|
|
9143
|
+
status: 202,
|
|
9144
|
+
params: [],
|
|
9145
|
+
query: null,
|
|
9146
|
+
request: feedbackRequest,
|
|
9147
|
+
response: feedbackResponse,
|
|
9148
|
+
errors: [...DEVELOPER_GUARD, "validation_error", "rate_limited"],
|
|
9149
|
+
transport: "http",
|
|
9150
|
+
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."
|
|
9151
|
+
},
|
|
9152
|
+
/* ----------------------------------------------------------- org (plan) */
|
|
9153
|
+
{
|
|
9154
|
+
method: "GET",
|
|
9155
|
+
path: "/api/org/plan",
|
|
9156
|
+
section: "org",
|
|
9157
|
+
summary: "Reads the org's plan: its limits, its usage against them, its add-ons and any change already queued.",
|
|
9158
|
+
audience: "developer",
|
|
9159
|
+
auth: "developer",
|
|
9160
|
+
rateLimited: false,
|
|
9161
|
+
ownerTier: false,
|
|
9162
|
+
status: 200,
|
|
9163
|
+
params: [],
|
|
9164
|
+
query: null,
|
|
9165
|
+
request: null,
|
|
9166
|
+
response: orgPlan,
|
|
9167
|
+
errors: [...DEVELOPER_GUARD],
|
|
9168
|
+
transport: "http",
|
|
9169
|
+
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."
|
|
9170
|
+
},
|
|
9171
|
+
{
|
|
9172
|
+
method: "PUT",
|
|
9173
|
+
path: "/api/org/plan/change",
|
|
9174
|
+
section: "org",
|
|
9175
|
+
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.",
|
|
9176
|
+
audience: "developer",
|
|
9177
|
+
auth: "developer",
|
|
9178
|
+
rateLimited: false,
|
|
9179
|
+
ownerTier: true,
|
|
9180
|
+
status: 200,
|
|
9181
|
+
params: [],
|
|
9182
|
+
query: null,
|
|
9183
|
+
request: planChangeRequest,
|
|
9184
|
+
response: orgPlan,
|
|
9185
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "validation_error", "plan_limit", "target_state_conflict"],
|
|
9186
|
+
transport: "http",
|
|
9187
|
+
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."
|
|
9188
|
+
},
|
|
9189
|
+
{
|
|
9190
|
+
method: "DELETE",
|
|
9191
|
+
path: "/api/org/plan/change",
|
|
9192
|
+
section: "org",
|
|
9193
|
+
summary: "Withdraws a plan change that was queued but has not taken effect yet.",
|
|
9194
|
+
audience: "developer",
|
|
9195
|
+
auth: "developer",
|
|
9196
|
+
rateLimited: false,
|
|
9197
|
+
ownerTier: true,
|
|
9198
|
+
status: 204,
|
|
9199
|
+
params: [],
|
|
9200
|
+
query: null,
|
|
9201
|
+
request: null,
|
|
9202
|
+
response: null,
|
|
9203
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "not_found"],
|
|
9204
|
+
transport: "http",
|
|
9205
|
+
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."
|
|
9206
|
+
},
|
|
9207
|
+
{
|
|
9208
|
+
method: "PATCH",
|
|
9209
|
+
path: "/api/admin/orgs/:id/plan",
|
|
9210
|
+
section: "org",
|
|
9211
|
+
summary: "Changes an org's plan, add-ons, limit overrides, currency or billing period as the operator.",
|
|
9212
|
+
audience: "internal",
|
|
9213
|
+
auth: "ops",
|
|
9214
|
+
rateLimited: true,
|
|
9215
|
+
ownerTier: false,
|
|
9216
|
+
status: 200,
|
|
9217
|
+
params: [{ name: "id", description: "The org's uuid, whose plan, add-ons or overrides the operator is changing." }],
|
|
9218
|
+
query: null,
|
|
9219
|
+
request: adminPlanChangeRequest,
|
|
9220
|
+
response: orgPlan,
|
|
9221
|
+
errors: ["unauthorized", "not_found", "validation_error", "plan_limit", "rate_limited"],
|
|
9222
|
+
transport: "http",
|
|
9223
|
+
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."
|
|
9224
|
+
},
|
|
8184
9225
|
/* ------------------------------------------------- assets (robot upload) */
|
|
8185
9226
|
{
|
|
8186
9227
|
method: "POST",
|
|
@@ -8196,9 +9237,9 @@ var ROUTES = [
|
|
|
8196
9237
|
query: null,
|
|
8197
9238
|
request: null,
|
|
8198
9239
|
response: asset,
|
|
8199
|
-
errors: ["unauthorized", "rate_limited", "validation_error", "not_found", "quota_exceeded", "bad_request"],
|
|
9240
|
+
errors: ["unauthorized", "rate_limited", "validation_error", "not_found", "file_too_large", "plan_limit", "quota_exceeded", "bad_request"],
|
|
8200
9241
|
transport: "http",
|
|
8201
|
-
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. **
|
|
9242
|
+
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."
|
|
8202
9243
|
},
|
|
8203
9244
|
/* ------------------------------------ realtime and bridge transports */
|
|
8204
9245
|
{
|
|
@@ -8323,13 +9364,19 @@ var ServerKeyCredentials = class {
|
|
|
8323
9364
|
function displayNameField(displayName) {
|
|
8324
9365
|
return displayName === void 0 ? {} : { display_name: displayName };
|
|
8325
9366
|
}
|
|
9367
|
+
function passwordField(password2) {
|
|
9368
|
+
return password2 === void 0 ? {} : { password: password2 };
|
|
9369
|
+
}
|
|
9370
|
+
function challengeField(challenge) {
|
|
9371
|
+
return challenge === void 0 ? {} : { challenge };
|
|
9372
|
+
}
|
|
8326
9373
|
function createPublicAuthCalls(http, appIdentifier2) {
|
|
8327
9374
|
return {
|
|
8328
9375
|
async register(input) {
|
|
8329
9376
|
const body = {
|
|
8330
9377
|
app_identifier: appIdentifier2,
|
|
8331
9378
|
email: input.email,
|
|
8332
|
-
|
|
9379
|
+
...passwordField(input.password),
|
|
8333
9380
|
...displayNameField(input.displayName)
|
|
8334
9381
|
};
|
|
8335
9382
|
await http.request("/api/client/register", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
@@ -8341,6 +9388,10 @@ function createPublicAuthCalls(http, appIdentifier2) {
|
|
|
8341
9388
|
async requestPasswordReset(email) {
|
|
8342
9389
|
const body = { app_identifier: appIdentifier2, email };
|
|
8343
9390
|
await http.request("/api/client/password/reset", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
9391
|
+
},
|
|
9392
|
+
async requestLoginCode(email) {
|
|
9393
|
+
const body = { app_identifier: appIdentifier2, email };
|
|
9394
|
+
await http.request("/api/client/login/code", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
8344
9395
|
}
|
|
8345
9396
|
};
|
|
8346
9397
|
}
|
|
@@ -8348,6 +9399,11 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8348
9399
|
async function storeSession(tokens) {
|
|
8349
9400
|
await tokenStore.save(tokens);
|
|
8350
9401
|
}
|
|
9402
|
+
async function completeSignIn(result) {
|
|
9403
|
+
if ("status" in result) return { status: result.status, challenge: result.challenge };
|
|
9404
|
+
await storeSession(result);
|
|
9405
|
+
return { status: "signed_in" };
|
|
9406
|
+
}
|
|
8351
9407
|
async function identity() {
|
|
8352
9408
|
return http.request("/api/client/me", {});
|
|
8353
9409
|
}
|
|
@@ -8355,13 +9411,18 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8355
9411
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8356
9412
|
async verifyEmail(token) {
|
|
8357
9413
|
const body = { token };
|
|
8358
|
-
const
|
|
8359
|
-
|
|
9414
|
+
const result = await http.request("/api/client/verify-email", { method: "POST", skipAuth: true, body });
|
|
9415
|
+
return completeSignIn(result);
|
|
8360
9416
|
},
|
|
8361
9417
|
async login(email, password2) {
|
|
8362
9418
|
const body = { app_identifier: appIdentifier2, email, password: password2 };
|
|
8363
|
-
const
|
|
8364
|
-
|
|
9419
|
+
const result = await http.request("/api/client/login", { method: "POST", skipAuth: true, body });
|
|
9420
|
+
return completeSignIn(result);
|
|
9421
|
+
},
|
|
9422
|
+
async verifyLoginCode(email, code) {
|
|
9423
|
+
const body = { app_identifier: appIdentifier2, email, code };
|
|
9424
|
+
const result = await http.request("/api/client/login/code/verify", { method: "POST", skipAuth: true, body });
|
|
9425
|
+
return completeSignIn(result);
|
|
8365
9426
|
},
|
|
8366
9427
|
async logout() {
|
|
8367
9428
|
const session = await tokenStore.load();
|
|
@@ -8375,6 +9436,13 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8375
9436
|
await tokenStore.save(null);
|
|
8376
9437
|
},
|
|
8377
9438
|
async me() {
|
|
9439
|
+
const session = await tokenStore.load();
|
|
9440
|
+
if (!session) {
|
|
9441
|
+
throw new FleetlessError(
|
|
9442
|
+
"no_session",
|
|
9443
|
+
"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."
|
|
9444
|
+
);
|
|
9445
|
+
}
|
|
8378
9446
|
return identity();
|
|
8379
9447
|
},
|
|
8380
9448
|
async changePassword(currentPassword, newPassword) {
|
|
@@ -8384,23 +9452,61 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8384
9452
|
},
|
|
8385
9453
|
async confirmPasswordReset(token, newPassword) {
|
|
8386
9454
|
const body = { token, new_password: newPassword };
|
|
8387
|
-
const
|
|
8388
|
-
|
|
9455
|
+
const result = await http.request("/api/client/password/reset/confirm", { method: "POST", skipAuth: true, body });
|
|
9456
|
+
return completeSignIn(result);
|
|
8389
9457
|
},
|
|
8390
9458
|
async acceptInvitation(input) {
|
|
8391
9459
|
const body = {
|
|
8392
9460
|
token: input.token,
|
|
8393
|
-
|
|
9461
|
+
...passwordField(input.password),
|
|
8394
9462
|
...displayNameField(input.displayName)
|
|
8395
9463
|
};
|
|
8396
|
-
const
|
|
9464
|
+
const result = await http.request("/api/client/invitations/accept", { method: "POST", skipAuth: true, body });
|
|
9465
|
+
return completeSignIn(result);
|
|
9466
|
+
},
|
|
9467
|
+
async verifyTwoFactor(input) {
|
|
9468
|
+
const body = {
|
|
9469
|
+
challenge: input.challenge,
|
|
9470
|
+
...input.code !== void 0 ? { code: input.code } : {},
|
|
9471
|
+
...input.recoveryCode !== void 0 ? { recovery_code: input.recoveryCode } : {}
|
|
9472
|
+
};
|
|
9473
|
+
const tokens = await http.request("/api/client/two-factor/verify", { method: "POST", skipAuth: true, body });
|
|
8397
9474
|
await storeSession(tokens);
|
|
8398
9475
|
},
|
|
9476
|
+
async beginTwoFactorSetup(input) {
|
|
9477
|
+
const challenge = input?.challenge;
|
|
9478
|
+
const body = challengeField(challenge);
|
|
9479
|
+
const response = await http.request("/api/client/two-factor/setup", {
|
|
9480
|
+
method: "POST",
|
|
9481
|
+
skipAuth: challenge !== void 0,
|
|
9482
|
+
body
|
|
9483
|
+
});
|
|
9484
|
+
return { secret: response.secret, otpauthUrl: response.otpauth_url };
|
|
9485
|
+
},
|
|
9486
|
+
async confirmTwoFactorSetup(input) {
|
|
9487
|
+
const body = { code: input.code, ...challengeField(input.challenge) };
|
|
9488
|
+
const response = await http.request("/api/client/two-factor/setup/confirm", {
|
|
9489
|
+
method: "POST",
|
|
9490
|
+
skipAuth: input.challenge !== void 0,
|
|
9491
|
+
body
|
|
9492
|
+
});
|
|
9493
|
+
await storeSession(response.session);
|
|
9494
|
+
return { recoveryCodes: response.recovery_codes };
|
|
9495
|
+
},
|
|
9496
|
+
async disableTwoFactor(code) {
|
|
9497
|
+
const body = { code };
|
|
9498
|
+
await http.request("/api/client/two-factor", { method: "DELETE", expectEmptyBody: true, body });
|
|
9499
|
+
},
|
|
8399
9500
|
async listProviders() {
|
|
8400
9501
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
8401
9502
|
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
8402
9503
|
return response.providers;
|
|
8403
9504
|
},
|
|
9505
|
+
async signInMethods() {
|
|
9506
|
+
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
9507
|
+
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
9508
|
+
return { password: response.sign_in_methods.password, emailCode: response.sign_in_methods.email_code };
|
|
9509
|
+
},
|
|
8404
9510
|
async beginOidcLogin(input) {
|
|
8405
9511
|
const state = generateState();
|
|
8406
9512
|
const codeVerifier = generateCodeVerifier();
|
|
@@ -8484,11 +9590,11 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8484
9590
|
const subject = option === "serverKey" ? "a server key" : "a supplied credential";
|
|
8485
9591
|
const holder = option === "serverKey" ? "a server-key client" : "a client with a supplied credential";
|
|
8486
9592
|
return {
|
|
8487
|
-
// **`register`, `resendVerification
|
|
8488
|
-
// allowed here** — see `createPublicAuthCalls`.
|
|
8489
|
-
// that name their own subject and answer
|
|
8490
|
-
// sign-up or forgot-password
|
|
8491
|
-
// already has.
|
|
9593
|
+
// **`register`, `resendVerification`, `requestPasswordReset` and
|
|
9594
|
+
// `requestLoginCode` are allowed here** — see `createPublicAuthCalls`.
|
|
9595
|
+
// They are public routes that name their own subject and answer
|
|
9596
|
+
// nothing, so a server-rendered sign-up or forgot-password (or
|
|
9597
|
+
// sign-in-by-code) page can use the one client its backend already has.
|
|
8492
9598
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8493
9599
|
async verifyEmail() {
|
|
8494
9600
|
serverKeyRefusal("verifyEmail", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
@@ -8496,6 +9602,9 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8496
9602
|
async login() {
|
|
8497
9603
|
serverKeyRefusal("login", `${subject} IS the credential; there is nothing to exchange`);
|
|
8498
9604
|
},
|
|
9605
|
+
async verifyLoginCode() {
|
|
9606
|
+
serverKeyRefusal("verifyLoginCode", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
9607
|
+
},
|
|
8499
9608
|
async logout() {
|
|
8500
9609
|
serverKeyRefusal("logout", `${subject} holds no session to end`);
|
|
8501
9610
|
},
|
|
@@ -8511,11 +9620,31 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8511
9620
|
async acceptInvitation() {
|
|
8512
9621
|
serverKeyRefusal("acceptInvitation", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
8513
9622
|
},
|
|
9623
|
+
async verifyTwoFactor() {
|
|
9624
|
+
serverKeyRefusal("verifyTwoFactor", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
9625
|
+
},
|
|
9626
|
+
async beginTwoFactorSetup() {
|
|
9627
|
+
serverKeyRefusal("beginTwoFactorSetup", `a two-factor setup is a person's own account's, and ${subject} is not a person's session`);
|
|
9628
|
+
},
|
|
9629
|
+
async confirmTwoFactorSetup() {
|
|
9630
|
+
serverKeyRefusal(
|
|
9631
|
+
"confirmTwoFactorSetup",
|
|
9632
|
+
`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`
|
|
9633
|
+
);
|
|
9634
|
+
},
|
|
9635
|
+
async disableTwoFactor() {
|
|
9636
|
+
serverKeyRefusal("disableTwoFactor", `turning an authenticator off is a person's own decision, and ${subject} is not a person`);
|
|
9637
|
+
},
|
|
8514
9638
|
async listProviders() {
|
|
8515
9639
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
8516
9640
|
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
8517
9641
|
return response.providers;
|
|
8518
9642
|
},
|
|
9643
|
+
async signInMethods() {
|
|
9644
|
+
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
9645
|
+
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
9646
|
+
return { password: response.sign_in_methods.password, emailCode: response.sign_in_methods.email_code };
|
|
9647
|
+
},
|
|
8519
9648
|
async beginOidcLogin() {
|
|
8520
9649
|
serverKeyRefusal("beginOidcLogin", "a federated sign-in is inherently an app user's browser flow");
|
|
8521
9650
|
},
|