@ref0/sdk 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browse-SaxCrclF.d.mts → browse-Dios11m0.d.mts} +2 -2
- package/dist/browser.d.mts +1 -1
- package/dist/client.d.mts +60 -6
- package/dist/client.mjs +725 -417
- package/dist/{index-BSPC_jQm.d.mts → index-DRFaIKvW.d.mts} +1343 -1104
- package/dist/index.d.mts +2 -2
- package/package.json +1 -1
package/dist/client.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Clock, Context, Cron, DateTime, Duration, Effect, Layer, Option, Predicate, Queue, Random, Result, Schema, SchemaIssue, SchemaTransformation, Stream, Struct, Tuple } from "effect";
|
|
1
|
+
import { Clock, Context, Cron, DateTime, Duration, Effect, Layer, Option, Predicate, Queue, Random, Redacted, Ref, Result, Schema, SchemaIssue, SchemaTransformation, Stream, Struct, Tuple } from "effect";
|
|
2
2
|
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity } from "effect/http-api";
|
|
3
|
-
import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
+
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
+
import { Base64Url } from "effect/encoding";
|
|
4
5
|
//#region ../contracts/src/typeid.ts
|
|
5
6
|
/**
|
|
6
7
|
* TypeIDs: a prefix, an underscore, then a UUID in 26 characters of Crockford's base32,
|
|
@@ -641,6 +642,402 @@ const AgentDetached = eventKind("agent.detached", Schema.Struct({
|
|
|
641
642
|
why: Schema.Literals(["revoked", "attach lost"])
|
|
642
643
|
}));
|
|
643
644
|
//#endregion
|
|
645
|
+
//#region ../contracts/src/connections.ts
|
|
646
|
+
/**
|
|
647
|
+
* Connections and their signals. A connection links a project to one outside service, to receive
|
|
648
|
+
* its signals: the service calls the connection's webhook, `/hooks/<connection>?token=<token>`,
|
|
649
|
+
* and the project's Project DO records each signal in the project's log, by the connection. A
|
|
650
|
+
* signal is short and structured, as every event is: what kind of thing happened and what it's
|
|
651
|
+
* about, both IDs, and a title and a link, both text IDs. The raw payload is never kept.
|
|
652
|
+
*/
|
|
653
|
+
/** Where a connection's webhook is called, on ref0's one origin, beside `/v1`. */
|
|
654
|
+
const hooksBase = "/hooks";
|
|
655
|
+
/** Whether a path is under the webhooks'. */
|
|
656
|
+
const isHooksPath = (pathname) => pathname.startsWith(`${hooksBase}/`);
|
|
657
|
+
/**
|
|
658
|
+
* A connection's name in its project, such as `sentry` or `deploys`: lowercase letters, digits
|
|
659
|
+
* and hyphens, from a letter, as a mention names an outside service. `ref0` names ref0's own, so
|
|
660
|
+
* no connection takes it. Two connections of a project are never named alike at once.
|
|
661
|
+
*/
|
|
662
|
+
const ConnectionName = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9-]{0,38}$/), Schema.makeFilter((name) => name !== "ref0" || "`ref0` names ref0’s own")).pipe(Schema.brand("ConnectionName"));
|
|
663
|
+
/**
|
|
664
|
+
* What reads a connection's deliveries: `webhook`, whose sender posts ref0's own shape, `{ kind,
|
|
665
|
+
* key, title?, url? }`; or `sentry`, whose sender is Sentry, posting its issue and alert
|
|
666
|
+
* webhooks, which its mapper reads. More kinds come one at a time, each with its mapper.
|
|
667
|
+
*/
|
|
668
|
+
const ConnectionKind = Schema.Literals(["webhook", "sentry"]);
|
|
669
|
+
/** The most characters in a signal's link: a longer one is withheld. */
|
|
670
|
+
const signalLinkLength = 2048;
|
|
671
|
+
/**
|
|
672
|
+
* An ID as a signal holds one: letters, digits and `:` `/` `.` `-` `_`, from a letter or a
|
|
673
|
+
* digit, so it can't carry a space, a quote, an instruction or a command's option.
|
|
674
|
+
*/
|
|
675
|
+
const signalIdPattern = (length) => new RegExp(`^[A-Za-z0-9][A-Za-z0-9:/._-]{0,${length - 1}}$`);
|
|
676
|
+
/** What happened, in the service's own word: `issue.created`, `deploy.finished`. */
|
|
677
|
+
const SignalKind = Schema.String.check(Schema.isPattern(signalIdPattern(64))).pipe(Schema.brand("SignalKind"));
|
|
678
|
+
/** What it's about, so a repeat can be recognised: an issue's ID, a release. */
|
|
679
|
+
const SignalKey = Schema.String.check(Schema.isPattern(signalIdPattern(128))).pipe(Schema.brand("SignalKey"));
|
|
680
|
+
/**
|
|
681
|
+
* Why a signal's title or link wasn't kept: it looked like a secret; a link that isn't `https`;
|
|
682
|
+
* or a link too long to keep whole.
|
|
683
|
+
*/
|
|
684
|
+
const SignalWithheldWhy = Schema.Literals([
|
|
685
|
+
"looks like a secret",
|
|
686
|
+
"not an https link",
|
|
687
|
+
"too long"
|
|
688
|
+
]);
|
|
689
|
+
/** A signal's title or link the connection gave, withheld, and why. */
|
|
690
|
+
const SignalWithheld = Schema.Struct({
|
|
691
|
+
title: Schema.optionalKey(SignalWithheldWhy),
|
|
692
|
+
url: Schema.optionalKey(SignalWithheldWhy)
|
|
693
|
+
});
|
|
694
|
+
/**
|
|
695
|
+
* A Sentry organization, by its slug as Sentry's URLs name it: lowercase letters, digits,
|
|
696
|
+
* hyphens and underscores, from a letter or a digit.
|
|
697
|
+
*/
|
|
698
|
+
const SentryOrg = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9_-]{0,49}$/)).pipe(Schema.brand("SentryOrg"));
|
|
699
|
+
/** Where a Sentry organization's data lives, and so which of Sentry's API hosts answers for it. */
|
|
700
|
+
const SentryRegion = Schema.Literals(["us", "de"]);
|
|
701
|
+
/** A connection added to the project, by the admin whose approval added it. */
|
|
702
|
+
const ConnectionAdded = eventKind("connection.added", Schema.Struct({
|
|
703
|
+
connection: ConnectionId,
|
|
704
|
+
name: ConnectionName,
|
|
705
|
+
kind: ConnectionKind
|
|
706
|
+
}));
|
|
707
|
+
/** A connection's token replaced, by the admin whose approval replaced it: the old one works no more. */
|
|
708
|
+
const ConnectionRotated = eventKind("connection.rotated", Schema.Struct({ connection: ConnectionId }));
|
|
709
|
+
/** A connection removed, by the admin whose approval removed it: its webhook takes nothing more. */
|
|
710
|
+
const ConnectionRemoved = eventKind("connection.removed", Schema.Struct({ connection: ConnectionId }));
|
|
711
|
+
/**
|
|
712
|
+
* A `sentry` connection's credential set, by the admin whose approval set it: the Sentry
|
|
713
|
+
* organization and region it reads, and that ref0 now holds a token for them, which only ref0's
|
|
714
|
+
* server reads, to call Sentry for a workflow. Never the token, nor its hash. Setting it again
|
|
715
|
+
* replaces the one before.
|
|
716
|
+
*/
|
|
717
|
+
const ConnectionCredentialSet = eventKind("connection.credential.set", Schema.Struct({
|
|
718
|
+
connection: ConnectionId,
|
|
719
|
+
org: SentryOrg,
|
|
720
|
+
region: SentryRegion
|
|
721
|
+
}));
|
|
722
|
+
/**
|
|
723
|
+
* A signal, received by a connection: delivered to its webhook, by `ref0:hooks`, or sent with
|
|
724
|
+
* `ref0 signal send` by the person or agent whose token sent it. `at` is when ref0 received it.
|
|
725
|
+
* A title or link that failed its check is withheld, and says why in `withheld`; a title too long
|
|
726
|
+
* is cut, and says so in `titleCut`. `dropped` counts the deliveries the connection dropped since
|
|
727
|
+
* its last signal: over a bound, or refused. A delivery's ID, if the service gave one, is in the
|
|
728
|
+
* envelope's `source` only as the SHA-256 of the connection and that ID.
|
|
729
|
+
*/
|
|
730
|
+
const SignalReceived = eventKind("signal.received", Schema.Struct({
|
|
731
|
+
connection: ConnectionId,
|
|
732
|
+
kind: SignalKind,
|
|
733
|
+
key: SignalKey,
|
|
734
|
+
title: Schema.optionalKey(TextId),
|
|
735
|
+
url: Schema.optionalKey(TextId),
|
|
736
|
+
withheld: Schema.optionalKey(SignalWithheld),
|
|
737
|
+
titleCut: Schema.optionalKey(Schema.Literal(true)),
|
|
738
|
+
dropped: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
|
739
|
+
at: Schema.DateTimeUtcFromMillis
|
|
740
|
+
}));
|
|
741
|
+
/**
|
|
742
|
+
* ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
|
|
743
|
+
* credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
|
|
744
|
+
* such as `https://1.2.3.999`, `https://[1::2::3]` or `https://ref0.test:99999`, so the platform's own URL parser must take
|
|
745
|
+
* it too. It's kept as written, case and any default port too: every path here is appended to it
|
|
746
|
+
* as it is.
|
|
747
|
+
*/
|
|
748
|
+
const ApiOrigin = Schema.String.check(Schema.isPattern(/^https?:\/\/(?:\[[0-9A-Fa-f:.]+\]|[^\s/?#\\@:[\]]+)(?::\d+)?$/), Schema.makeFilter((origin) => URL.canParse(origin) || "a URL the platform can parse")).pipe(Schema.brand("ApiOrigin"));
|
|
749
|
+
/** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
|
|
750
|
+
const signInBase = "/auth";
|
|
751
|
+
/** Whether a path is signing in's. */
|
|
752
|
+
const isSignInPath = (pathname) => pathname.startsWith(`${signInBase}/`);
|
|
753
|
+
/**
|
|
754
|
+
* A route under `signInBase`, by its method: its `path` from the origin's root, as a request
|
|
755
|
+
* names it, and its `pathUnderBase`, as Better Auth names its endpoints.
|
|
756
|
+
*/
|
|
757
|
+
const signInRoute = (method, pathUnderBase) => ({
|
|
758
|
+
method,
|
|
759
|
+
pathUnderBase,
|
|
760
|
+
path: `${signInBase}${pathUnderBase}`
|
|
761
|
+
});
|
|
762
|
+
/** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
|
|
763
|
+
const signInRoutes = {
|
|
764
|
+
gitHubSignIn: signInRoute("POST", "/sign-in/social"),
|
|
765
|
+
gitHubCallback: signInRoute("GET", "/callback/github"),
|
|
766
|
+
signOut: signInRoute("POST", "/sign-out")
|
|
767
|
+
};
|
|
768
|
+
/**
|
|
769
|
+
* The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
|
|
770
|
+
* token endpoint, registering a public client and revoking a token.
|
|
771
|
+
*/
|
|
772
|
+
const oauthRoutes = {
|
|
773
|
+
metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
|
|
774
|
+
authorize: signInRoute("GET", "/oauth2/authorize"),
|
|
775
|
+
token: signInRoute("POST", "/oauth2/token"),
|
|
776
|
+
register: signInRoute("POST", "/oauth2/register"),
|
|
777
|
+
revoke: signInRoute("POST", "/oauth2/revoke")
|
|
778
|
+
};
|
|
779
|
+
signInRoute("POST", "/oauth2/consent");
|
|
780
|
+
[...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
|
|
781
|
+
/**
|
|
782
|
+
* Where an outside MCP client looks for `/mcp`'s OAuth (layer 4, PR 11a): RFC 9728's metadata
|
|
783
|
+
* for the resource, and RFC 8414's for the server at `/auth`, each at the origin's root.
|
|
784
|
+
*/
|
|
785
|
+
const oauthDiscoveryPaths = ["/.well-known/oauth-protected-resource/mcp", `${oauthRoutes.metadata.pathUnderBase}${signInBase}`];
|
|
786
|
+
/**
|
|
787
|
+
* Where an outside service finds identity tokens' issuer and its public keys, each at the
|
|
788
|
+
* origin's root: the issuer's discovery document, and its JWKS. Only these two: every other path
|
|
789
|
+
* under `/.well-known/` is a page's.
|
|
790
|
+
*/
|
|
791
|
+
const identityDiscoveryRoutes = {
|
|
792
|
+
configuration: "/.well-known/openid-configuration",
|
|
793
|
+
keys: "/.well-known/jwks.json"
|
|
794
|
+
};
|
|
795
|
+
/** Whether a path is one of identity tokens' two published documents. */
|
|
796
|
+
const isIdentityDiscoveryPath = (pathname) => pathname === identityDiscoveryRoutes.configuration || pathname === identityDiscoveryRoutes.keys;
|
|
797
|
+
/**
|
|
798
|
+
* Whether a path is Worker "server"'s to answer, not a page's: the API's, signing in's, a
|
|
799
|
+
* connection's webhook, OAuth's metadata, or identity tokens' published documents.
|
|
800
|
+
*/
|
|
801
|
+
const isRef0Path = (pathname) => isSignInPath(pathname) || isHooksPath(pathname) || pathname === "/v1" || pathname.startsWith(`/v1/`) || oauthDiscoveryPaths.includes(pathname) || isIdentityDiscoveryPath(pathname);
|
|
802
|
+
/** Where an agent's session asks for an identity token. */
|
|
803
|
+
const identityTokenPath = "/v1/me/identity-token";
|
|
804
|
+
/** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
|
|
805
|
+
const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
|
|
806
|
+
/** `https://`, a host name, then maybe `:` and a port with no leading zero. */
|
|
807
|
+
const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
|
|
808
|
+
/**
|
|
809
|
+
* Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
|
|
810
|
+
* the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
|
|
811
|
+
* the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
|
|
812
|
+
*/
|
|
813
|
+
const parsesAsWritten = (audience) => {
|
|
814
|
+
const parts = audiencePattern.exec(audience);
|
|
815
|
+
if (parts === null || !URL.canParse(audience)) return false;
|
|
816
|
+
const url = new URL(audience);
|
|
817
|
+
const port = parts[2] ?? "";
|
|
818
|
+
return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
|
|
822
|
+
* with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
|
|
823
|
+
* case and any port too, so `https://verifier.example:443` is another audience than
|
|
824
|
+
* `https://verifier.example`. ref0 never fetches or resolves it.
|
|
825
|
+
*/
|
|
826
|
+
const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
|
|
827
|
+
/** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
|
|
828
|
+
const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
|
|
829
|
+
/** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
|
|
830
|
+
const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
|
|
831
|
+
/** What asking for an identity token answers: the token, and when it expires, its `exp`. */
|
|
832
|
+
const IssuedIdentityToken = Schema.Struct({
|
|
833
|
+
token: IdentityToken,
|
|
834
|
+
expiresAt: Schema.DateTimeUtcFromMillis
|
|
835
|
+
});
|
|
836
|
+
/**
|
|
837
|
+
* An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
|
|
838
|
+
* token, naming its issuance in ref0's log without naming any row or secret.
|
|
839
|
+
*/
|
|
840
|
+
const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
|
|
841
|
+
/**
|
|
842
|
+
* A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
|
|
843
|
+
* person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
|
|
844
|
+
* audience, different at every other, and carrying no time.
|
|
845
|
+
*/
|
|
846
|
+
const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
|
|
847
|
+
/** A session, as one audience knows it, made as a person's is. */
|
|
848
|
+
const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
|
|
849
|
+
/** The accountable person, last in a token's chain: only their pairwise ID. */
|
|
850
|
+
const PersonActor = Schema.Struct({ sub: PairwisePersonId });
|
|
851
|
+
/** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
|
|
852
|
+
const sessionActing = (act) => Schema.Struct({
|
|
853
|
+
sub: PairwiseSessionId,
|
|
854
|
+
act
|
|
855
|
+
});
|
|
856
|
+
const oneSession = sessionActing(PersonActor);
|
|
857
|
+
const twoSessions = sessionActing(oneSession);
|
|
858
|
+
const threeSessions = sessionActing(twoSessions);
|
|
859
|
+
/**
|
|
860
|
+
* A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
|
|
861
|
+
* then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
|
|
862
|
+
* allows the agent in `sub`, never an actor.
|
|
863
|
+
*/
|
|
864
|
+
const IdentityActor = Schema.Union([
|
|
865
|
+
oneSession,
|
|
866
|
+
twoSessions,
|
|
867
|
+
threeSessions,
|
|
868
|
+
sessionActing(threeSessions)
|
|
869
|
+
]);
|
|
870
|
+
/** A time in a token, in whole UTC epoch seconds. */
|
|
871
|
+
const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
|
|
872
|
+
Schema.Struct({
|
|
873
|
+
iss: ApiOrigin,
|
|
874
|
+
sub: AgentId,
|
|
875
|
+
aud: IdentityAudience,
|
|
876
|
+
iat: EpochSeconds,
|
|
877
|
+
exp: EpochSeconds,
|
|
878
|
+
jti: IdentityTokenId,
|
|
879
|
+
act: IdentityActor
|
|
880
|
+
}).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
|
|
881
|
+
/**
|
|
882
|
+
* A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
|
|
883
|
+
* never names two keys.
|
|
884
|
+
*/
|
|
885
|
+
const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
|
|
886
|
+
Schema.Struct({
|
|
887
|
+
alg: Schema.Literal("ES256"),
|
|
888
|
+
kid: SigningKeyId,
|
|
889
|
+
typ: Schema.Literal("JWT")
|
|
890
|
+
});
|
|
891
|
+
/** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
|
|
892
|
+
const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
|
|
893
|
+
/** One public key in the JWKS: exactly these fields, and never a private one. */
|
|
894
|
+
const PublicSigningKey = Schema.Struct({
|
|
895
|
+
kty: Schema.Literal("EC"),
|
|
896
|
+
crv: Schema.Literal("P-256"),
|
|
897
|
+
x: Coordinate,
|
|
898
|
+
y: Coordinate,
|
|
899
|
+
use: Schema.Literal("sig"),
|
|
900
|
+
alg: Schema.Literal("ES256"),
|
|
901
|
+
kid: SigningKeyId
|
|
902
|
+
});
|
|
903
|
+
Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
|
|
904
|
+
Schema.Struct({
|
|
905
|
+
issuer: ApiOrigin,
|
|
906
|
+
jwks_uri: Schema.String,
|
|
907
|
+
subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
|
|
908
|
+
id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
|
|
909
|
+
claims_supported: Schema.Tuple([
|
|
910
|
+
Schema.Literal("iss"),
|
|
911
|
+
Schema.Literal("sub"),
|
|
912
|
+
Schema.Literal("aud"),
|
|
913
|
+
Schema.Literal("exp"),
|
|
914
|
+
Schema.Literal("iat"),
|
|
915
|
+
Schema.Literal("jti"),
|
|
916
|
+
Schema.Literal("act")
|
|
917
|
+
])
|
|
918
|
+
});
|
|
919
|
+
/**
|
|
920
|
+
* The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
|
|
921
|
+
* `IdentityAudience` says. The refused text is never echoed.
|
|
922
|
+
*/
|
|
923
|
+
var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
|
|
924
|
+
/**
|
|
925
|
+
* Less than a whole second is left before the session, a parent or the credential expires, or
|
|
926
|
+
* the token would expire before it's recorded: the next prompt renews or replaces the session.
|
|
927
|
+
*/
|
|
928
|
+
var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
|
|
929
|
+
/**
|
|
930
|
+
* ref0 signed an identity token for one of a project's sessions: in that project's log, on the
|
|
931
|
+
* session's thread, with ref0's own IDs for the session, its agent and its accountable person,
|
|
932
|
+
* and the token's `jti`, never the token. It proves ref0 made the token, not that any service
|
|
933
|
+
* received or used it. A failed append discards the token, so every token has its event.
|
|
934
|
+
*/
|
|
935
|
+
const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
|
|
936
|
+
session: SessionId,
|
|
937
|
+
agent: AgentRef,
|
|
938
|
+
/** The person whose attachment authorized the session's chain, last in the token's `act`. */
|
|
939
|
+
accountable: PersonId,
|
|
940
|
+
jti: IdentityTokenId,
|
|
941
|
+
audience: IdentityAudience,
|
|
942
|
+
issuer: ApiOrigin,
|
|
943
|
+
keyId: SigningKeyId,
|
|
944
|
+
issuedAt: Schema.DateTimeUtcFromMillis,
|
|
945
|
+
expiresAt: Schema.DateTimeUtcFromMillis
|
|
946
|
+
}));
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region ../contracts/src/proofs.ts
|
|
949
|
+
/**
|
|
950
|
+
* Proofs of possession: a credential bound to a key, and the proof that comes with each request
|
|
951
|
+
* made with it, RFC 9449's DPoP. A bound credential, a host grant or a session's token, names its
|
|
952
|
+
* key's thumbprint as `cnf`, which only ref0's own state says, never a caller. Each request sends
|
|
953
|
+
* the token as `Authorization: DPoP <token>` and one `DPoP` header: a compact JWS, signed with
|
|
954
|
+
* ES256 by that key, over the request's method and URL, when it was made, and the token's hash.
|
|
955
|
+
* There's no server nonce and no replay store: a captured token and proof can be used again on
|
|
956
|
+
* the same method and path while the proof's time is in the window, and a token alone can't.
|
|
957
|
+
*/
|
|
958
|
+
/** A P-256 key's RFC 7638 thumbprint: SHA-256 over its required members, in base64url. */
|
|
959
|
+
const KeyThumbprint = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("KeyThumbprint"));
|
|
960
|
+
/** The key a credential is bound to, by its thumbprint: RFC 7800's `cnf`, as `{ jkt }`. */
|
|
961
|
+
const KeyBinding = Schema.Struct({ jkt: KeyThumbprint });
|
|
962
|
+
/** Hosts ref0's origin may name over plain HTTP, for development on one machine. */
|
|
963
|
+
const loopback = /* @__PURE__ */ new Set([
|
|
964
|
+
"localhost",
|
|
965
|
+
"127.0.0.1",
|
|
966
|
+
"[::1]"
|
|
967
|
+
]);
|
|
968
|
+
/**
|
|
969
|
+
* ref0's origin as a proof names it: HTTPS, or plain HTTP only on loopback, for development. A
|
|
970
|
+
* client sends a proof, and ref0 takes one, for no other.
|
|
971
|
+
*/
|
|
972
|
+
const ProofOrigin = ApiOrigin.check(Schema.makeFilter((origin) => {
|
|
973
|
+
const url = URL.parse(origin);
|
|
974
|
+
return url?.protocol === "https:" || url?.protocol === "http:" && loopback.has(url.hostname) || "HTTPS, or HTTP on loopback";
|
|
975
|
+
})).pipe(Schema.brand("ProofOrigin"));
|
|
976
|
+
/** The most a proof takes, as its compact JWS, before anything else reads it. */
|
|
977
|
+
const dpopProofMaxBytes = 2048;
|
|
978
|
+
const utf8$1 = new TextEncoder();
|
|
979
|
+
const encodeJson$2 = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
|
|
980
|
+
/**
|
|
981
|
+
* An object with exactly `fields`, of at most `maxBytes` as JSON, whatever a decode's options
|
|
982
|
+
* say: an exact `Struct` alone drops a member it doesn't know unless its decode is told to refuse
|
|
983
|
+
* one, so the members are checked on the object as it came, before the struct reads them.
|
|
984
|
+
*/
|
|
985
|
+
const exactly = (fields, maxBytes) => {
|
|
986
|
+
const names = new Set(Object.keys(fields));
|
|
987
|
+
return Schema.Record(Schema.String, Schema.Unknown).check(Schema.makeFilter((members) => Object.keys(members).every((name) => names.has(name)) || `only ${[...names].join(", ")}`), Schema.makeFilter((members) => utf8$1.encode(encodeJson$2(members)).byteLength <= maxBytes || `at most ${maxBytes} bytes as JSON`)).pipe(Schema.decodeTo(Schema.Struct(fields), SchemaTransformation.passthroughSupertype()));
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* A P-256 public key as a JWK: exactly `kty`, `crv`, `x` and `y`, so never a private part, a
|
|
991
|
+
* key reference or another algorithm's key. Whether its point is on the curve is for Web Crypto
|
|
992
|
+
* to say, when it imports it.
|
|
993
|
+
*/
|
|
994
|
+
const PublicJwk = exactly({
|
|
995
|
+
kty: Schema.Literal("EC"),
|
|
996
|
+
crv: Schema.Literal("P-256"),
|
|
997
|
+
x: PublicSigningKey.fields.x,
|
|
998
|
+
y: PublicSigningKey.fields.y
|
|
999
|
+
}, 512);
|
|
1000
|
+
/** A proof's protected header: its type, ES256, and the public key that signed it. */
|
|
1001
|
+
const DpopProofHeader = exactly({
|
|
1002
|
+
typ: Schema.Literal("dpop+jwt"),
|
|
1003
|
+
alg: Schema.Literal("ES256"),
|
|
1004
|
+
jwk: PublicJwk
|
|
1005
|
+
}, dpopProofMaxBytes);
|
|
1006
|
+
/** A proof's `jti`: a fresh UUIDv4 for each proof. Nothing keeps it to refuse a repeat. */
|
|
1007
|
+
const DpopProofId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("DpopProofId"));
|
|
1008
|
+
/**
|
|
1009
|
+
* A proof's claims: its ID; the request's method, and its URL with no query or fragment, as its
|
|
1010
|
+
* client sent it, after the URL was built; when it was made, in whole seconds; and, with a token,
|
|
1011
|
+
* `ath`, the SHA-256 of the whole token in base64url.
|
|
1012
|
+
*/
|
|
1013
|
+
const DpopProofClaims = exactly({
|
|
1014
|
+
jti: DpopProofId,
|
|
1015
|
+
htm: Schema.String.check(Schema.isPattern(/^[A-Z]{1,16}$/)),
|
|
1016
|
+
htu: Schema.String.check(Schema.isMaxLength(dpopProofMaxBytes)),
|
|
1017
|
+
iat: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
1018
|
+
ath: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)))
|
|
1019
|
+
}, dpopProofMaxBytes);
|
|
1020
|
+
/**
|
|
1021
|
+
* A bound credential came with a proof that's missing, malformed, stale, from another key, or
|
|
1022
|
+
* for another request, or came as a bearer token, or a proof came with no `DPoP` credential to
|
|
1023
|
+
* go with it. ref0 answers it before any handler runs, so nothing was done. A machine whose clock
|
|
1024
|
+
* is off signs again once, by the answer's `Date`. Also what a machine answers itself when the key
|
|
1025
|
+
* its credential is bound to is gone, without sending anything.
|
|
1026
|
+
*/
|
|
1027
|
+
var InvalidDpopProof = class extends Schema.TaggedError()("InvalidDpopProof", {
|
|
1028
|
+
error: Schema.Literal("invalid_dpop_proof"),
|
|
1029
|
+
nextStep: Schema.String
|
|
1030
|
+
}, { httpApiStatus: 401 }) {};
|
|
1031
|
+
new InvalidDpopProof({
|
|
1032
|
+
error: "invalid_dpop_proof",
|
|
1033
|
+
nextStep: "If this machine’s key was lost, run ref0 connect in your own terminal, then send the next prompt."
|
|
1034
|
+
});
|
|
1035
|
+
/**
|
|
1036
|
+
* The key store a request's proof is signed in didn't sign it: locked, unavailable or out of
|
|
1037
|
+
* time. Nothing was sent. A client's own, never an answer of ref0's.
|
|
1038
|
+
*/
|
|
1039
|
+
var KeyStoreUnavailable = class extends Schema.TaggedError()("KeyStoreUnavailable", { nextStep: Schema.String }) {};
|
|
1040
|
+
//#endregion
|
|
644
1041
|
//#region ../contracts/src/credential-events.ts
|
|
645
1042
|
/**
|
|
646
1043
|
* A person's credentials, in that person's own log. Never the token: only its ID, its kind
|
|
@@ -661,7 +1058,9 @@ const TokenDelegated = eventKind("token.delegated", Schema.Struct({
|
|
|
661
1058
|
session: Schema.optionalKey(SessionId),
|
|
662
1059
|
/** The credential this one was made under, when there is one. */
|
|
663
1060
|
parent: Schema.optionalKey(CredentialRef),
|
|
664
|
-
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis)
|
|
1061
|
+
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
1062
|
+
/** The key it's bound to, by its thumbprint, for a host grant made with one. */
|
|
1063
|
+
cnf: Schema.optionalKey(KeyBinding)
|
|
665
1064
|
}));
|
|
666
1065
|
const TokenRevoked = eventKind("token.revoked", Schema.Struct({
|
|
667
1066
|
credential: CredentialRef,
|
|
@@ -976,6 +1375,11 @@ const CredentialFacts = Schema.Struct({
|
|
|
976
1375
|
target: Schema.optionalKey(TargetGeneration)
|
|
977
1376
|
}),
|
|
978
1377
|
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
1378
|
+
/**
|
|
1379
|
+
* The key it's bound to, for a host grant or a session's token made with one: every request
|
|
1380
|
+
* with it must carry a proof that key signed. ref0's own state says it, never a caller.
|
|
1381
|
+
*/
|
|
1382
|
+
cnf: Schema.optionalKey(KeyBinding),
|
|
979
1383
|
/** What it may do, and where. Rows a reader can't decode were left out: they grant nothing. */
|
|
980
1384
|
scopes: Schema.Array(Scope),
|
|
981
1385
|
/**
|
|
@@ -2680,103 +3084,6 @@ Schema.Struct({ intent: PublishIntentId });
|
|
|
2680
3084
|
Schema.Struct({ intent: PublishIntentId });
|
|
2681
3085
|
Schema.Union([PublishState, Schema.Struct({ none: Schema.Literal(true) })]);
|
|
2682
3086
|
//#endregion
|
|
2683
|
-
//#region ../contracts/src/connections.ts
|
|
2684
|
-
/**
|
|
2685
|
-
* Connections and their signals. A connection links a project to one outside service, to receive
|
|
2686
|
-
* its signals: the service calls the connection's webhook, `/hooks/<connection>?token=<token>`,
|
|
2687
|
-
* and the project's Project DO records each signal in the project's log, by the connection. A
|
|
2688
|
-
* signal is short and structured, as every event is: what kind of thing happened and what it's
|
|
2689
|
-
* about, both IDs, and a title and a link, both text IDs. The raw payload is never kept.
|
|
2690
|
-
*/
|
|
2691
|
-
/** Where a connection's webhook is called, on ref0's one origin, beside `/v1`. */
|
|
2692
|
-
const hooksBase = "/hooks";
|
|
2693
|
-
/** Whether a path is under the webhooks'. */
|
|
2694
|
-
const isHooksPath = (pathname) => pathname.startsWith(`${hooksBase}/`);
|
|
2695
|
-
/**
|
|
2696
|
-
* A connection's name in its project, such as `sentry` or `deploys`: lowercase letters, digits
|
|
2697
|
-
* and hyphens, from a letter, as a mention names an outside service. `ref0` names ref0's own, so
|
|
2698
|
-
* no connection takes it. Two connections of a project are never named alike at once.
|
|
2699
|
-
*/
|
|
2700
|
-
const ConnectionName = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9-]{0,38}$/), Schema.makeFilter((name) => name !== "ref0" || "`ref0` names ref0’s own")).pipe(Schema.brand("ConnectionName"));
|
|
2701
|
-
/**
|
|
2702
|
-
* What reads a connection's deliveries: `webhook`, whose sender posts ref0's own shape, `{ kind,
|
|
2703
|
-
* key, title?, url? }`; or `sentry`, whose sender is Sentry, posting its issue and alert
|
|
2704
|
-
* webhooks, which its mapper reads. More kinds come one at a time, each with its mapper.
|
|
2705
|
-
*/
|
|
2706
|
-
const ConnectionKind = Schema.Literals(["webhook", "sentry"]);
|
|
2707
|
-
/** The most characters in a signal's link: a longer one is withheld. */
|
|
2708
|
-
const signalLinkLength = 2048;
|
|
2709
|
-
/**
|
|
2710
|
-
* An ID as a signal holds one: letters, digits and `:` `/` `.` `-` `_`, from a letter or a
|
|
2711
|
-
* digit, so it can't carry a space, a quote, an instruction or a command's option.
|
|
2712
|
-
*/
|
|
2713
|
-
const signalIdPattern = (length) => new RegExp(`^[A-Za-z0-9][A-Za-z0-9:/._-]{0,${length - 1}}$`);
|
|
2714
|
-
/** What happened, in the service's own word: `issue.created`, `deploy.finished`. */
|
|
2715
|
-
const SignalKind = Schema.String.check(Schema.isPattern(signalIdPattern(64))).pipe(Schema.brand("SignalKind"));
|
|
2716
|
-
/** What it's about, so a repeat can be recognised: an issue's ID, a release. */
|
|
2717
|
-
const SignalKey = Schema.String.check(Schema.isPattern(signalIdPattern(128))).pipe(Schema.brand("SignalKey"));
|
|
2718
|
-
/**
|
|
2719
|
-
* Why a signal's title or link wasn't kept: it looked like a secret; a link that isn't `https`;
|
|
2720
|
-
* or a link too long to keep whole.
|
|
2721
|
-
*/
|
|
2722
|
-
const SignalWithheldWhy = Schema.Literals([
|
|
2723
|
-
"looks like a secret",
|
|
2724
|
-
"not an https link",
|
|
2725
|
-
"too long"
|
|
2726
|
-
]);
|
|
2727
|
-
/** A signal's title or link the connection gave, withheld, and why. */
|
|
2728
|
-
const SignalWithheld = Schema.Struct({
|
|
2729
|
-
title: Schema.optionalKey(SignalWithheldWhy),
|
|
2730
|
-
url: Schema.optionalKey(SignalWithheldWhy)
|
|
2731
|
-
});
|
|
2732
|
-
/**
|
|
2733
|
-
* A Sentry organization, by its slug as Sentry's URLs name it: lowercase letters, digits,
|
|
2734
|
-
* hyphens and underscores, from a letter or a digit.
|
|
2735
|
-
*/
|
|
2736
|
-
const SentryOrg = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9_-]{0,49}$/)).pipe(Schema.brand("SentryOrg"));
|
|
2737
|
-
/** Where a Sentry organization's data lives, and so which of Sentry's API hosts answers for it. */
|
|
2738
|
-
const SentryRegion = Schema.Literals(["us", "de"]);
|
|
2739
|
-
/** A connection added to the project, by the admin whose approval added it. */
|
|
2740
|
-
const ConnectionAdded = eventKind("connection.added", Schema.Struct({
|
|
2741
|
-
connection: ConnectionId,
|
|
2742
|
-
name: ConnectionName,
|
|
2743
|
-
kind: ConnectionKind
|
|
2744
|
-
}));
|
|
2745
|
-
/** A connection's token replaced, by the admin whose approval replaced it: the old one works no more. */
|
|
2746
|
-
const ConnectionRotated = eventKind("connection.rotated", Schema.Struct({ connection: ConnectionId }));
|
|
2747
|
-
/** A connection removed, by the admin whose approval removed it: its webhook takes nothing more. */
|
|
2748
|
-
const ConnectionRemoved = eventKind("connection.removed", Schema.Struct({ connection: ConnectionId }));
|
|
2749
|
-
/**
|
|
2750
|
-
* A `sentry` connection's credential set, by the admin whose approval set it: the Sentry
|
|
2751
|
-
* organization and region it reads, and that ref0 now holds a token for them, which only ref0's
|
|
2752
|
-
* server reads, to call Sentry for a workflow. Never the token, nor its hash. Setting it again
|
|
2753
|
-
* replaces the one before.
|
|
2754
|
-
*/
|
|
2755
|
-
const ConnectionCredentialSet = eventKind("connection.credential.set", Schema.Struct({
|
|
2756
|
-
connection: ConnectionId,
|
|
2757
|
-
org: SentryOrg,
|
|
2758
|
-
region: SentryRegion
|
|
2759
|
-
}));
|
|
2760
|
-
/**
|
|
2761
|
-
* A signal, received by a connection: delivered to its webhook, by `ref0:hooks`, or sent with
|
|
2762
|
-
* `ref0 signal send` by the person or agent whose token sent it. `at` is when ref0 received it.
|
|
2763
|
-
* A title or link that failed its check is withheld, and says why in `withheld`; a title too long
|
|
2764
|
-
* is cut, and says so in `titleCut`. `dropped` counts the deliveries the connection dropped since
|
|
2765
|
-
* its last signal: over a bound, or refused. A delivery's ID, if the service gave one, is in the
|
|
2766
|
-
* envelope's `source` only as the SHA-256 of the connection and that ID.
|
|
2767
|
-
*/
|
|
2768
|
-
const SignalReceived = eventKind("signal.received", Schema.Struct({
|
|
2769
|
-
connection: ConnectionId,
|
|
2770
|
-
kind: SignalKind,
|
|
2771
|
-
key: SignalKey,
|
|
2772
|
-
title: Schema.optionalKey(TextId),
|
|
2773
|
-
url: Schema.optionalKey(TextId),
|
|
2774
|
-
withheld: Schema.optionalKey(SignalWithheld),
|
|
2775
|
-
titleCut: Schema.optionalKey(Schema.Literal(true)),
|
|
2776
|
-
dropped: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
|
2777
|
-
at: Schema.DateTimeUtcFromMillis
|
|
2778
|
-
}));
|
|
2779
|
-
//#endregion
|
|
2780
3087
|
//#region ../contracts/src/date-time.ts
|
|
2781
3088
|
/** A UTC date decoded from a string and encoded in ISO format. */
|
|
2782
3089
|
const Timestamp = Schema.DateTimeUtcFromString;
|
|
@@ -5308,8 +5615,26 @@ Schema.Union([
|
|
|
5308
5615
|
NotPermitted,
|
|
5309
5616
|
DeliveryOutsideWindow
|
|
5310
5617
|
]);
|
|
5618
|
+
/**
|
|
5619
|
+
* Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
|
|
5620
|
+
* project's visibility, which a session's routes read who sees the project by.
|
|
5621
|
+
*/
|
|
5622
|
+
const SessionPlace = Schema.Struct({
|
|
5623
|
+
owner: OwnerId,
|
|
5624
|
+
project: ProjectId,
|
|
5625
|
+
visibility: Visibility,
|
|
5626
|
+
thread: ThreadId,
|
|
5627
|
+
threadNumber: ThreadNumber,
|
|
5628
|
+
session: SessionId,
|
|
5629
|
+
number: SessionNumber,
|
|
5630
|
+
/** Whether it's still going: not ended, and its lease not run out. */
|
|
5631
|
+
live: Schema.Boolean
|
|
5632
|
+
});
|
|
5311
5633
|
Schema.Struct({ session: SessionId });
|
|
5312
|
-
Schema.Struct({
|
|
5634
|
+
Schema.Union([Schema.Struct({
|
|
5635
|
+
live: Schema.Literal(true),
|
|
5636
|
+
place: SessionPlace
|
|
5637
|
+
}), Schema.Struct({ live: Schema.Literal(false) })]);
|
|
5313
5638
|
Schema.Struct({
|
|
5314
5639
|
...commandFields,
|
|
5315
5640
|
project: ProjectId,
|
|
@@ -6297,8 +6622,8 @@ var ApprovalStale = class extends Schema.TaggedError()("ApprovalStale", { nextSt
|
|
|
6297
6622
|
var PreviewNotReady = class extends Schema.TaggedError()("PreviewNotReady", { nextStep: Schema.String }, { httpApiStatus: 503 }) {};
|
|
6298
6623
|
//#endregion
|
|
6299
6624
|
//#region ../contracts/src/passkeys.ts
|
|
6300
|
-
const Base64Url = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]*$/));
|
|
6301
|
-
Schema.Struct({ challenge: Base64Url.check(Schema.isMinLength(1)) });
|
|
6625
|
+
const Base64Url$1 = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]*$/));
|
|
6626
|
+
Schema.Struct({ challenge: Base64Url$1.check(Schema.isMinLength(1)) });
|
|
6302
6627
|
/** How a browser reaches a passkey's authenticator, as WebAuthn names it. */
|
|
6303
6628
|
const PasskeyTransport = Schema.Literals([
|
|
6304
6629
|
"ble",
|
|
@@ -6310,7 +6635,7 @@ const PasskeyTransport = Schema.Literals([
|
|
|
6310
6635
|
"usb"
|
|
6311
6636
|
]);
|
|
6312
6637
|
const CredentialDescriptor = Schema.Struct({
|
|
6313
|
-
id: Base64Url,
|
|
6638
|
+
id: Base64Url$1,
|
|
6314
6639
|
type: Schema.Literal("public-key"),
|
|
6315
6640
|
transports: Schema.optionalKey(Schema.Array(PasskeyTransport))
|
|
6316
6641
|
});
|
|
@@ -6321,11 +6646,11 @@ const PasskeyCreationOptions = Schema.Struct({
|
|
|
6321
6646
|
name: Schema.String
|
|
6322
6647
|
}),
|
|
6323
6648
|
user: Schema.Struct({
|
|
6324
|
-
id: Base64Url,
|
|
6649
|
+
id: Base64Url$1,
|
|
6325
6650
|
name: Schema.String,
|
|
6326
6651
|
displayName: Schema.String
|
|
6327
6652
|
}),
|
|
6328
|
-
challenge: Base64Url,
|
|
6653
|
+
challenge: Base64Url$1,
|
|
6329
6654
|
pubKeyCredParams: Schema.Array(Schema.Struct({
|
|
6330
6655
|
type: Schema.Literal("public-key"),
|
|
6331
6656
|
alg: Schema.Int
|
|
@@ -6364,23 +6689,23 @@ const PasskeyCreationOptions = Schema.Struct({
|
|
|
6364
6689
|
* as its WebAuthn library leaves it.
|
|
6365
6690
|
*/
|
|
6366
6691
|
const PasskeyCreated = Schema.Struct({
|
|
6367
|
-
id: Base64Url,
|
|
6368
|
-
rawId: Base64Url,
|
|
6692
|
+
id: Base64Url$1,
|
|
6693
|
+
rawId: Base64Url$1,
|
|
6369
6694
|
type: Schema.Literal("public-key"),
|
|
6370
6695
|
response: Schema.Struct({
|
|
6371
|
-
clientDataJSON: Base64Url,
|
|
6372
|
-
attestationObject: Base64Url,
|
|
6373
|
-
authenticatorData: Schema.optional(Base64Url),
|
|
6696
|
+
clientDataJSON: Base64Url$1,
|
|
6697
|
+
attestationObject: Base64Url$1,
|
|
6698
|
+
authenticatorData: Schema.optional(Base64Url$1),
|
|
6374
6699
|
transports: Schema.optional(Schema.Array(PasskeyTransport)),
|
|
6375
6700
|
publicKeyAlgorithm: Schema.optional(Schema.Int),
|
|
6376
|
-
publicKey: Schema.optional(Base64Url)
|
|
6701
|
+
publicKey: Schema.optional(Base64Url$1)
|
|
6377
6702
|
}),
|
|
6378
6703
|
authenticatorAttachment: Schema.optional(Schema.Literals(["platform", "cross-platform"])),
|
|
6379
6704
|
clientExtensionResults: Schema.Struct({ credProps: Schema.optionalKey(Schema.Struct({ rk: Schema.optionalKey(Schema.Boolean) })) })
|
|
6380
6705
|
});
|
|
6381
6706
|
/** What ref0 hands a browser to answer with a passkey: WebAuthn's request options, as JSON. */
|
|
6382
6707
|
const PasskeyRequestOptions = Schema.Struct({
|
|
6383
|
-
challenge: Base64Url,
|
|
6708
|
+
challenge: Base64Url$1,
|
|
6384
6709
|
timeout: Schema.optionalKey(Schema.Int),
|
|
6385
6710
|
rpId: Schema.optionalKey(Schema.String),
|
|
6386
6711
|
allowCredentials: Schema.optionalKey(Schema.Array(CredentialDescriptor)),
|
|
@@ -6400,14 +6725,14 @@ const PasskeyRequestOptions = Schema.Struct({
|
|
|
6400
6725
|
* as JSON. A field the browser doesn't have may be missing, or there as `undefined`.
|
|
6401
6726
|
*/
|
|
6402
6727
|
const PasskeyAsserted = Schema.Struct({
|
|
6403
|
-
id: Base64Url,
|
|
6404
|
-
rawId: Base64Url,
|
|
6728
|
+
id: Base64Url$1,
|
|
6729
|
+
rawId: Base64Url$1,
|
|
6405
6730
|
type: Schema.Literal("public-key"),
|
|
6406
6731
|
response: Schema.Struct({
|
|
6407
|
-
clientDataJSON: Base64Url,
|
|
6408
|
-
authenticatorData: Base64Url,
|
|
6409
|
-
signature: Base64Url,
|
|
6410
|
-
userHandle: Schema.optional(Base64Url)
|
|
6732
|
+
clientDataJSON: Base64Url$1,
|
|
6733
|
+
authenticatorData: Base64Url$1,
|
|
6734
|
+
signature: Base64Url$1,
|
|
6735
|
+
userHandle: Schema.optional(Base64Url$1)
|
|
6411
6736
|
}),
|
|
6412
6737
|
authenticatorAttachment: Schema.optional(Schema.Literals(["platform", "cross-platform"])),
|
|
6413
6738
|
clientExtensionResults: Schema.Struct({})
|
|
@@ -6459,10 +6784,15 @@ const RequestFolder = Schema.Struct({
|
|
|
6459
6784
|
secretHash: SecretHash,
|
|
6460
6785
|
replaces: Schema.optionalKey(TokenId)
|
|
6461
6786
|
});
|
|
6462
|
-
/**
|
|
6787
|
+
/**
|
|
6788
|
+
* One to eight config folders, each with its own grant secret, and the machine key its grants are
|
|
6789
|
+
* bound to: its public half, which a proof that key signed comes with. A request without one makes
|
|
6790
|
+
* grants bound to no key, as an older ref0 asks, until binding is required.
|
|
6791
|
+
*/
|
|
6463
6792
|
const RequestAttachment = Schema.Struct({
|
|
6464
6793
|
machine: MachineId,
|
|
6465
6794
|
machineName: MachineName,
|
|
6795
|
+
publicKey: Schema.optionalKey(PublicJwk),
|
|
6466
6796
|
folders: Schema.Array(RequestFolder).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
|
|
6467
6797
|
collectHash: SecretHash,
|
|
6468
6798
|
linkHash: SecretHash,
|
|
@@ -6576,12 +6906,15 @@ const RequestOrganization = Schema.Struct({
|
|
|
6576
6906
|
/**
|
|
6577
6907
|
* The result collected with the machine's two proofs: each included folder's identity and grant,
|
|
6578
6908
|
* its agent's owner name and organizations, and excluded paths. Confirmation checks each saved
|
|
6579
|
-
* grant's live deadline in the Owner DO.
|
|
6909
|
+
* grant's live deadline in the Owner DO. Once a yes connected and the machine hasn't sent its
|
|
6910
|
+
* answer code, waiting says until when: `confirmBy`, when ref0 revokes each grant the machine
|
|
6911
|
+
* hasn't confirmed.
|
|
6580
6912
|
*/
|
|
6581
6913
|
const RequestState = Schema.Union([
|
|
6582
6914
|
Schema.Struct({
|
|
6583
6915
|
state: Schema.Literal("waiting"),
|
|
6584
|
-
expiresAt: Timestamp
|
|
6916
|
+
expiresAt: Timestamp,
|
|
6917
|
+
confirmBy: Schema.optionalKey(Timestamp)
|
|
6585
6918
|
}),
|
|
6586
6919
|
Schema.Struct({
|
|
6587
6920
|
state: Schema.Literal("connected"),
|
|
@@ -6655,6 +6988,7 @@ Schema.Struct({
|
|
|
6655
6988
|
request: ApprovalId,
|
|
6656
6989
|
machine: MachineId,
|
|
6657
6990
|
machineName: MachineName,
|
|
6991
|
+
cnf: Schema.optionalKey(KeyBinding),
|
|
6658
6992
|
browserSession: TokenId,
|
|
6659
6993
|
personGeneration: Generation,
|
|
6660
6994
|
folders: Schema.Array(ConnectFolderCommand).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
|
|
@@ -10215,117 +10549,56 @@ const GrantView = Schema.Struct({
|
|
|
10215
10549
|
role: GrantRole,
|
|
10216
10550
|
grantedBy: ActorRef,
|
|
10217
10551
|
grantedAt: Schema.DateTimeUtcFromMillis
|
|
10218
|
-
});
|
|
10219
|
-
/** A page of the grants a reader may see, in the order they were given, and where the next starts. */
|
|
10220
|
-
const GrantPage = Schema.Struct({
|
|
10221
|
-
grants: Schema.Array(GrantView),
|
|
10222
|
-
nextAfter: Schema.optionalKey(EventId)
|
|
10223
|
-
});
|
|
10224
|
-
Schema.Struct({
|
|
10225
|
-
reader: PersonId,
|
|
10226
|
-
after: Schema.optionalKey(EventId),
|
|
10227
|
-
limit: PageLimit
|
|
10228
|
-
});
|
|
10229
|
-
/**
|
|
10230
|
-
* A grant as its Owner DO holds it: what it's on by the names it keeps, a thread by its scope and
|
|
10231
|
-
* number; who holds it, by ID, for the Worker to name.
|
|
10232
|
-
*/
|
|
10233
|
-
const GrantFound = Schema.Struct({
|
|
10234
|
-
grant: EventId,
|
|
10235
|
-
subject: Schema.Union([
|
|
10236
|
-
Schema.Struct({ kind: Schema.Literal("owner") }),
|
|
10237
|
-
Schema.Struct({
|
|
10238
|
-
kind: Schema.Literal("project"),
|
|
10239
|
-
project: ProjectName
|
|
10240
|
-
}),
|
|
10241
|
-
Schema.Struct({
|
|
10242
|
-
kind: Schema.Literal("thread"),
|
|
10243
|
-
scope: ThreadScope,
|
|
10244
|
-
number: ThreadNumber
|
|
10245
|
-
}),
|
|
10246
|
-
Schema.Struct({
|
|
10247
|
-
kind: Schema.Literal("agent"),
|
|
10248
|
-
handle: AgentHandle
|
|
10249
|
-
})
|
|
10250
|
-
]),
|
|
10251
|
-
principal: Principal,
|
|
10252
|
-
role: GrantRole,
|
|
10253
|
-
grantedBy: ActorRef,
|
|
10254
|
-
grantedAt: Schema.DateTimeUtcFromMillis
|
|
10255
|
-
});
|
|
10256
|
-
Schema.Struct({
|
|
10257
|
-
grants: Schema.Array(GrantFound),
|
|
10258
|
-
nextAfter: Schema.optionalKey(EventId)
|
|
10259
|
-
});
|
|
10260
|
-
Schema.Struct({
|
|
10261
|
-
project: ProjectId,
|
|
10262
|
-
number: ThreadNumber
|
|
10263
|
-
});
|
|
10264
|
-
Schema.Union([Schema.Struct({
|
|
10265
|
-
found: Schema.Literal(true),
|
|
10266
|
-
thread: ThreadId
|
|
10267
|
-
}), Schema.Struct({ found: Schema.Literal(false) })]);
|
|
10268
|
-
/**
|
|
10269
|
-
* ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
|
|
10270
|
-
* credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
|
|
10271
|
-
* such as `https://1.2.3.999`, `https://[1::2::3]` or `https://ref0.test:99999`, so the platform's own URL parser must take
|
|
10272
|
-
* it too. It's kept as written, case and any default port too: every path here is appended to it
|
|
10273
|
-
* as it is.
|
|
10274
|
-
*/
|
|
10275
|
-
const ApiOrigin = Schema.String.check(Schema.isPattern(/^https?:\/\/(?:\[[0-9A-Fa-f:.]+\]|[^\s/?#\\@:[\]]+)(?::\d+)?$/), Schema.makeFilter((origin) => URL.canParse(origin) || "a URL the platform can parse")).pipe(Schema.brand("ApiOrigin"));
|
|
10276
|
-
/** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
|
|
10277
|
-
const signInBase = "/auth";
|
|
10278
|
-
/** Whether a path is signing in's. */
|
|
10279
|
-
const isSignInPath = (pathname) => pathname.startsWith(`${signInBase}/`);
|
|
10280
|
-
/**
|
|
10281
|
-
* A route under `signInBase`, by its method: its `path` from the origin's root, as a request
|
|
10282
|
-
* names it, and its `pathUnderBase`, as Better Auth names its endpoints.
|
|
10283
|
-
*/
|
|
10284
|
-
const signInRoute = (method, pathUnderBase) => ({
|
|
10285
|
-
method,
|
|
10286
|
-
pathUnderBase,
|
|
10287
|
-
path: `${signInBase}${pathUnderBase}`
|
|
10288
|
-
});
|
|
10289
|
-
/** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
|
|
10290
|
-
const signInRoutes = {
|
|
10291
|
-
gitHubSignIn: signInRoute("POST", "/sign-in/social"),
|
|
10292
|
-
gitHubCallback: signInRoute("GET", "/callback/github"),
|
|
10293
|
-
signOut: signInRoute("POST", "/sign-out")
|
|
10294
|
-
};
|
|
10295
|
-
/**
|
|
10296
|
-
* The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
|
|
10297
|
-
* token endpoint, registering a public client and revoking a token.
|
|
10298
|
-
*/
|
|
10299
|
-
const oauthRoutes = {
|
|
10300
|
-
metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
|
|
10301
|
-
authorize: signInRoute("GET", "/oauth2/authorize"),
|
|
10302
|
-
token: signInRoute("POST", "/oauth2/token"),
|
|
10303
|
-
register: signInRoute("POST", "/oauth2/register"),
|
|
10304
|
-
revoke: signInRoute("POST", "/oauth2/revoke")
|
|
10305
|
-
};
|
|
10306
|
-
signInRoute("POST", "/oauth2/consent");
|
|
10307
|
-
[...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
|
|
10308
|
-
/**
|
|
10309
|
-
* Where an outside MCP client looks for `/mcp`'s OAuth (layer 4, PR 11a): RFC 9728's metadata
|
|
10310
|
-
* for the resource, and RFC 8414's for the server at `/auth`, each at the origin's root.
|
|
10311
|
-
*/
|
|
10312
|
-
const oauthDiscoveryPaths = ["/.well-known/oauth-protected-resource/mcp", `${oauthRoutes.metadata.pathUnderBase}${signInBase}`];
|
|
10313
|
-
/**
|
|
10314
|
-
* Where an outside service finds identity tokens' issuer and its public keys, each at the
|
|
10315
|
-
* origin's root: the issuer's discovery document, and its JWKS. Only these two: every other path
|
|
10316
|
-
* under `/.well-known/` is a page's.
|
|
10317
|
-
*/
|
|
10318
|
-
const identityDiscoveryRoutes = {
|
|
10319
|
-
configuration: "/.well-known/openid-configuration",
|
|
10320
|
-
keys: "/.well-known/jwks.json"
|
|
10321
|
-
};
|
|
10322
|
-
/** Whether a path is one of identity tokens' two published documents. */
|
|
10323
|
-
const isIdentityDiscoveryPath = (pathname) => pathname === identityDiscoveryRoutes.configuration || pathname === identityDiscoveryRoutes.keys;
|
|
10552
|
+
});
|
|
10553
|
+
/** A page of the grants a reader may see, in the order they were given, and where the next starts. */
|
|
10554
|
+
const GrantPage = Schema.Struct({
|
|
10555
|
+
grants: Schema.Array(GrantView),
|
|
10556
|
+
nextAfter: Schema.optionalKey(EventId)
|
|
10557
|
+
});
|
|
10558
|
+
Schema.Struct({
|
|
10559
|
+
reader: PersonId,
|
|
10560
|
+
after: Schema.optionalKey(EventId),
|
|
10561
|
+
limit: PageLimit
|
|
10562
|
+
});
|
|
10324
10563
|
/**
|
|
10325
|
-
*
|
|
10326
|
-
*
|
|
10564
|
+
* A grant as its Owner DO holds it: what it's on by the names it keeps, a thread by its scope and
|
|
10565
|
+
* number; who holds it, by ID, for the Worker to name.
|
|
10327
10566
|
*/
|
|
10328
|
-
const
|
|
10567
|
+
const GrantFound = Schema.Struct({
|
|
10568
|
+
grant: EventId,
|
|
10569
|
+
subject: Schema.Union([
|
|
10570
|
+
Schema.Struct({ kind: Schema.Literal("owner") }),
|
|
10571
|
+
Schema.Struct({
|
|
10572
|
+
kind: Schema.Literal("project"),
|
|
10573
|
+
project: ProjectName
|
|
10574
|
+
}),
|
|
10575
|
+
Schema.Struct({
|
|
10576
|
+
kind: Schema.Literal("thread"),
|
|
10577
|
+
scope: ThreadScope,
|
|
10578
|
+
number: ThreadNumber
|
|
10579
|
+
}),
|
|
10580
|
+
Schema.Struct({
|
|
10581
|
+
kind: Schema.Literal("agent"),
|
|
10582
|
+
handle: AgentHandle
|
|
10583
|
+
})
|
|
10584
|
+
]),
|
|
10585
|
+
principal: Principal,
|
|
10586
|
+
role: GrantRole,
|
|
10587
|
+
grantedBy: ActorRef,
|
|
10588
|
+
grantedAt: Schema.DateTimeUtcFromMillis
|
|
10589
|
+
});
|
|
10590
|
+
Schema.Struct({
|
|
10591
|
+
grants: Schema.Array(GrantFound),
|
|
10592
|
+
nextAfter: Schema.optionalKey(EventId)
|
|
10593
|
+
});
|
|
10594
|
+
Schema.Struct({
|
|
10595
|
+
project: ProjectId,
|
|
10596
|
+
number: ThreadNumber
|
|
10597
|
+
});
|
|
10598
|
+
Schema.Union([Schema.Struct({
|
|
10599
|
+
found: Schema.Literal(true),
|
|
10600
|
+
thread: ThreadId
|
|
10601
|
+
}), Schema.Struct({ found: Schema.Literal(false) })]);
|
|
10329
10602
|
//#endregion
|
|
10330
10603
|
//#region ../contracts/src/live-calls.ts
|
|
10331
10604
|
/**
|
|
@@ -10974,22 +11247,75 @@ const TurnTokens = Schema.Union([Schema.Struct({
|
|
|
10974
11247
|
known: Schema.Literal("read"),
|
|
10975
11248
|
models: Schema.Array(ModelTokens).check(Schema.isMaxLength(16), Schema.makeFilter((models) => new Set(models.map((each) => each.model)).size === models.length || "each model once"))
|
|
10976
11249
|
}), Schema.Struct({ known: Schema.Literal("unknown") })]);
|
|
11250
|
+
const Name = Schema.String.check(Schema.isBetweenLength(1, 128));
|
|
11251
|
+
const Millis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
11252
|
+
/**
|
|
11253
|
+
* How long a turn's tool calls ran, summed, and its longest call, by tool: Claude Code's own
|
|
11254
|
+
* `duration_ms` for each, and, for Codex, whose hooks say none, from its `PreToolUse` to its
|
|
11255
|
+
* `PostToolUse` by `ref0 mcp`'s clock. Only calls that ended count: one with no end, as one a
|
|
11256
|
+
* `PreToolUse` answer denied or a Codex call cut short, isn't in it. `unknown`, never zero, when a
|
|
11257
|
+
* call that ended can't be timed, as one whose end says no time and whose start wasn't seen.
|
|
11258
|
+
*/
|
|
11259
|
+
const ToolTime = Schema.Union([Schema.Struct({
|
|
11260
|
+
known: Schema.Literal("timed"),
|
|
11261
|
+
totalMs: Millis,
|
|
11262
|
+
/** None when the turn called no tool. */
|
|
11263
|
+
longest: Schema.optionalKey(Schema.Struct({
|
|
11264
|
+
tool: Name,
|
|
11265
|
+
ms: Millis
|
|
11266
|
+
}))
|
|
11267
|
+
}), Schema.Struct({ known: Schema.Literal("unknown") })]);
|
|
11268
|
+
/**
|
|
11269
|
+
* A compaction of the session's context during a turn: whether it was `auto` or `manual`, where
|
|
11270
|
+
* the harness says, as Claude Code does and Codex doesn't, and how long after ref0 started the
|
|
11271
|
+
* turn it happened, by the harness's own transcript, where both are known.
|
|
11272
|
+
*/
|
|
11273
|
+
const Compaction = Schema.Struct({
|
|
11274
|
+
trigger: Schema.optionalKey(Schema.Literals(["auto", "manual"])),
|
|
11275
|
+
afterMs: Schema.optionalKey(Millis)
|
|
11276
|
+
});
|
|
11277
|
+
/**
|
|
11278
|
+
* A turn's context, read from the session's own transcript as its tokens are: `peak`, the most
|
|
11279
|
+
* one request of the session's own sent, its cached tokens included, none for a turn with no
|
|
11280
|
+
* request, and each compaction. `unknown`, with neither, where the transcript wasn't read whole.
|
|
11281
|
+
* A subagent's context is its own, and isn't in it.
|
|
11282
|
+
*/
|
|
11283
|
+
const TurnContext = Schema.Union([Schema.Struct({
|
|
11284
|
+
known: Schema.Literal("read"),
|
|
11285
|
+
peak: Schema.optionalKey(TokenCount),
|
|
11286
|
+
compactions: Schema.Array(Compaction).check(Schema.isMaxLength(100))
|
|
11287
|
+
}), Schema.Struct({ known: Schema.Literal("unknown") })]);
|
|
11288
|
+
/**
|
|
11289
|
+
* One of the harness's own subagents that a turn ran, such as Claude Code's Task agents or
|
|
11290
|
+
* Codex's: its type, where the harness names one, and its own tokens, which the turn's tokens
|
|
11291
|
+
* already include, so the turn's less its subagents' are the session's own. Its tokens, and the
|
|
11292
|
+
* turn's, are `unknown` when the harness kept no transcript of it. Not a child session: ref0
|
|
11293
|
+
* knows it only from its `SubagentStop` and its transcript.
|
|
11294
|
+
*/
|
|
11295
|
+
const TurnSubagent = Schema.Struct({
|
|
11296
|
+
type: Schema.optionalKey(Name),
|
|
11297
|
+
tokens: TurnTokens
|
|
11298
|
+
});
|
|
10977
11299
|
/**
|
|
10978
|
-
* A turn's facts but its tokens, which `TurnFacts` adds for whoever sends them
|
|
10979
|
-
* carries only to those who may see them.
|
|
11300
|
+
* A turn's facts but its tokens and its subagents', which `TurnFacts` adds for whoever sends them
|
|
11301
|
+
* and `turn.ended` carries only to those who may see them.
|
|
10980
11302
|
*/
|
|
10981
11303
|
const turnFactsFields = {
|
|
10982
11304
|
/** The turn's model, where the harness said it: Codex in every hook. */
|
|
10983
|
-
model: Schema.optionalKey(
|
|
11305
|
+
model: Schema.optionalKey(Name),
|
|
10984
11306
|
modelSource: ModelSource,
|
|
10985
11307
|
/** Tool calls by tool name. */
|
|
10986
|
-
tools: Schema.Record(
|
|
11308
|
+
tools: Schema.Record(Name, Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
|
|
10987
11309
|
/** The files its edits touched, by repo and path, at most 50; `more` counts the rest. */
|
|
10988
11310
|
addresses: Schema.Array(Schema.Struct({
|
|
10989
11311
|
repo: RepoName,
|
|
10990
11312
|
path: RepoPath
|
|
10991
11313
|
})).check(Schema.isMaxLength(50)),
|
|
10992
|
-
more: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
11314
|
+
more: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
11315
|
+
toolTime: ToolTime,
|
|
11316
|
+
context: TurnContext,
|
|
11317
|
+
/** How many subagents past the first 100 the turn ran. */
|
|
11318
|
+
moreSubagents: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
10993
11319
|
};
|
|
10994
11320
|
/**
|
|
10995
11321
|
* What `ref0 mcp` saw of a turn, sent at its `Stop`s and when the next one starts, and again as
|
|
@@ -11001,16 +11327,23 @@ const turnFactsFields = {
|
|
|
11001
11327
|
const TurnFacts = Schema.Struct({
|
|
11002
11328
|
...turnFactsFields,
|
|
11003
11329
|
tokens: TurnTokens,
|
|
11004
|
-
|
|
11330
|
+
/** The turn's subagents, in the order their transcripts were first read, at most 100. */
|
|
11331
|
+
subagents: Schema.Array(TurnSubagent).check(Schema.isMaxLength(100)),
|
|
11332
|
+
stoppedAfterMs: Schema.optionalKey(Millis)
|
|
11005
11333
|
});
|
|
11006
11334
|
/**
|
|
11007
|
-
* A turn's facts as `turn.ended` shows them: its tokens only to an admin of
|
|
11008
|
-
* and to the session's accountable person. A thread's totals still go to
|
|
11009
|
-
* `ThreadUsage`, so on a thread with one person's sessions they are that
|
|
11335
|
+
* A turn's facts as `turn.ended` shows them: its tokens and its subagents' only to an admin of
|
|
11336
|
+
* the project's owner and to the session's accountable person. A thread's totals still go to
|
|
11337
|
+
* every reader of it, in `ThreadUsage`, so on a thread with one person's sessions they are that
|
|
11338
|
+
* person's usage.
|
|
11010
11339
|
*/
|
|
11011
11340
|
const EndedTurnFacts = Schema.Struct({
|
|
11012
11341
|
...turnFactsFields,
|
|
11013
|
-
tokens: Schema.optionalKey(TurnTokens)
|
|
11342
|
+
tokens: Schema.optionalKey(TurnTokens),
|
|
11343
|
+
subagents: Schema.Array(Schema.Struct({
|
|
11344
|
+
type: TurnSubagent.fields.type,
|
|
11345
|
+
tokens: Schema.optionalKey(TurnTokens)
|
|
11346
|
+
})).check(Schema.isMaxLength(100))
|
|
11014
11347
|
});
|
|
11015
11348
|
/**
|
|
11016
11349
|
* `POST /v1/sessions/{session}/turns`: a new turn, for the harness's own turn ID, so a retry, or
|
|
@@ -11638,151 +11971,6 @@ Schema.Struct({
|
|
|
11638
11971
|
name: RepoName
|
|
11639
11972
|
}))
|
|
11640
11973
|
});
|
|
11641
|
-
/** Where an agent's session asks for an identity token. */
|
|
11642
|
-
const identityTokenPath = "/v1/me/identity-token";
|
|
11643
|
-
/** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
|
|
11644
|
-
const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
|
|
11645
|
-
/** `https://`, a host name, then maybe `:` and a port with no leading zero. */
|
|
11646
|
-
const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
|
|
11647
|
-
/**
|
|
11648
|
-
* Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
|
|
11649
|
-
* the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
|
|
11650
|
-
* the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
|
|
11651
|
-
*/
|
|
11652
|
-
const parsesAsWritten = (audience) => {
|
|
11653
|
-
const parts = audiencePattern.exec(audience);
|
|
11654
|
-
if (parts === null || !URL.canParse(audience)) return false;
|
|
11655
|
-
const url = new URL(audience);
|
|
11656
|
-
const port = parts[2] ?? "";
|
|
11657
|
-
return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
|
|
11658
|
-
};
|
|
11659
|
-
/**
|
|
11660
|
-
* Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
|
|
11661
|
-
* with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
|
|
11662
|
-
* case and any port too, so `https://verifier.example:443` is another audience than
|
|
11663
|
-
* `https://verifier.example`. ref0 never fetches or resolves it.
|
|
11664
|
-
*/
|
|
11665
|
-
const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
|
|
11666
|
-
/** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
|
|
11667
|
-
const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
|
|
11668
|
-
/** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
|
|
11669
|
-
const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
|
|
11670
|
-
/** What asking for an identity token answers: the token, and when it expires, its `exp`. */
|
|
11671
|
-
const IssuedIdentityToken = Schema.Struct({
|
|
11672
|
-
token: IdentityToken,
|
|
11673
|
-
expiresAt: Schema.DateTimeUtcFromMillis
|
|
11674
|
-
});
|
|
11675
|
-
/**
|
|
11676
|
-
* An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
|
|
11677
|
-
* token, naming its issuance in ref0's log without naming any row or secret.
|
|
11678
|
-
*/
|
|
11679
|
-
const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
|
|
11680
|
-
/**
|
|
11681
|
-
* A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
|
|
11682
|
-
* person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
|
|
11683
|
-
* audience, different at every other, and carrying no time.
|
|
11684
|
-
*/
|
|
11685
|
-
const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
|
|
11686
|
-
/** A session, as one audience knows it, made as a person's is. */
|
|
11687
|
-
const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
|
|
11688
|
-
/** The accountable person, last in a token's chain: only their pairwise ID. */
|
|
11689
|
-
const PersonActor = Schema.Struct({ sub: PairwisePersonId });
|
|
11690
|
-
/** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
|
|
11691
|
-
const sessionActing = (act) => Schema.Struct({
|
|
11692
|
-
sub: PairwiseSessionId,
|
|
11693
|
-
act
|
|
11694
|
-
});
|
|
11695
|
-
const oneSession = sessionActing(PersonActor);
|
|
11696
|
-
const twoSessions = sessionActing(oneSession);
|
|
11697
|
-
const threeSessions = sessionActing(twoSessions);
|
|
11698
|
-
/**
|
|
11699
|
-
* A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
|
|
11700
|
-
* then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
|
|
11701
|
-
* allows the agent in `sub`, never an actor.
|
|
11702
|
-
*/
|
|
11703
|
-
const IdentityActor = Schema.Union([
|
|
11704
|
-
oneSession,
|
|
11705
|
-
twoSessions,
|
|
11706
|
-
threeSessions,
|
|
11707
|
-
sessionActing(threeSessions)
|
|
11708
|
-
]);
|
|
11709
|
-
/** A time in a token, in whole UTC epoch seconds. */
|
|
11710
|
-
const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
|
|
11711
|
-
Schema.Struct({
|
|
11712
|
-
iss: ApiOrigin,
|
|
11713
|
-
sub: AgentId,
|
|
11714
|
-
aud: IdentityAudience,
|
|
11715
|
-
iat: EpochSeconds,
|
|
11716
|
-
exp: EpochSeconds,
|
|
11717
|
-
jti: IdentityTokenId,
|
|
11718
|
-
act: IdentityActor
|
|
11719
|
-
}).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
|
|
11720
|
-
/**
|
|
11721
|
-
* A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
|
|
11722
|
-
* never names two keys.
|
|
11723
|
-
*/
|
|
11724
|
-
const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
|
|
11725
|
-
Schema.Struct({
|
|
11726
|
-
alg: Schema.Literal("ES256"),
|
|
11727
|
-
kid: SigningKeyId,
|
|
11728
|
-
typ: Schema.Literal("JWT")
|
|
11729
|
-
});
|
|
11730
|
-
/** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
|
|
11731
|
-
const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
|
|
11732
|
-
/** One public key in the JWKS: exactly these fields, and never a private one. */
|
|
11733
|
-
const PublicSigningKey = Schema.Struct({
|
|
11734
|
-
kty: Schema.Literal("EC"),
|
|
11735
|
-
crv: Schema.Literal("P-256"),
|
|
11736
|
-
x: Coordinate,
|
|
11737
|
-
y: Coordinate,
|
|
11738
|
-
use: Schema.Literal("sig"),
|
|
11739
|
-
alg: Schema.Literal("ES256"),
|
|
11740
|
-
kid: SigningKeyId
|
|
11741
|
-
});
|
|
11742
|
-
Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
|
|
11743
|
-
Schema.Struct({
|
|
11744
|
-
issuer: ApiOrigin,
|
|
11745
|
-
jwks_uri: Schema.String,
|
|
11746
|
-
subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
|
|
11747
|
-
id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
|
|
11748
|
-
claims_supported: Schema.Tuple([
|
|
11749
|
-
Schema.Literal("iss"),
|
|
11750
|
-
Schema.Literal("sub"),
|
|
11751
|
-
Schema.Literal("aud"),
|
|
11752
|
-
Schema.Literal("exp"),
|
|
11753
|
-
Schema.Literal("iat"),
|
|
11754
|
-
Schema.Literal("jti"),
|
|
11755
|
-
Schema.Literal("act")
|
|
11756
|
-
])
|
|
11757
|
-
});
|
|
11758
|
-
/**
|
|
11759
|
-
* The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
|
|
11760
|
-
* `IdentityAudience` says. The refused text is never echoed.
|
|
11761
|
-
*/
|
|
11762
|
-
var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
|
|
11763
|
-
/**
|
|
11764
|
-
* Less than a whole second is left before the session, a parent or the credential expires, or
|
|
11765
|
-
* the token would expire before it's recorded: the next prompt renews or replaces the session.
|
|
11766
|
-
*/
|
|
11767
|
-
var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
|
|
11768
|
-
/**
|
|
11769
|
-
* ref0 signed an identity token for one of a project's sessions: in that project's log, on the
|
|
11770
|
-
* session's thread, with ref0's own IDs for the session, its agent and its accountable person,
|
|
11771
|
-
* and the token's `jti`, never the token. It proves ref0 made the token, not that any service
|
|
11772
|
-
* received or used it. A failed append discards the token, so every token has its event.
|
|
11773
|
-
*/
|
|
11774
|
-
const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
|
|
11775
|
-
session: SessionId,
|
|
11776
|
-
agent: AgentRef,
|
|
11777
|
-
/** The person whose attachment authorized the session's chain, last in the token's `act`. */
|
|
11778
|
-
accountable: PersonId,
|
|
11779
|
-
jti: IdentityTokenId,
|
|
11780
|
-
audience: IdentityAudience,
|
|
11781
|
-
issuer: ApiOrigin,
|
|
11782
|
-
keyId: SigningKeyId,
|
|
11783
|
-
issuedAt: Schema.DateTimeUtcFromMillis,
|
|
11784
|
-
expiresAt: Schema.DateTimeUtcFromMillis
|
|
11785
|
-
}));
|
|
11786
11974
|
Schema.Literals([
|
|
11787
11975
|
"info/refs",
|
|
11788
11976
|
"git-upload-pack",
|
|
@@ -11810,6 +11998,11 @@ Schema.Union([
|
|
|
11810
11998
|
Schema.Struct({
|
|
11811
11999
|
reader: Reader,
|
|
11812
12000
|
name: RepoName,
|
|
12001
|
+
/**
|
|
12002
|
+
* For a fork's path, the scope it names: a project, by its ID or its name, or, with none, the
|
|
12003
|
+
* owner's own scope, which holds an organization's org threads.
|
|
12004
|
+
*/
|
|
12005
|
+
scope: Schema.optionalKey(Schema.Struct({ project: Schema.optionalKey(ProjectRef) })),
|
|
11813
12006
|
push: Schema.optionalKey(Schema.Struct({
|
|
11814
12007
|
thread: ThreadId,
|
|
11815
12008
|
session: SessionId
|
|
@@ -11823,7 +12016,9 @@ const GitTarget = Schema.Struct({
|
|
|
11823
12016
|
* their sessions' forks, and the repo's `main`. Unset for one that reads the repo's project as
|
|
11824
12017
|
* a whole.
|
|
11825
12018
|
*/
|
|
11826
|
-
threads: Schema.optionalKey(Schema.Array(ThreadId))
|
|
12019
|
+
threads: Schema.optionalKey(Schema.Array(ThreadId)),
|
|
12020
|
+
/** For a fork's path, the project its scope resolved to: a project's, or the owner's own. */
|
|
12021
|
+
scope: Schema.optionalKey(ProjectId)
|
|
11827
12022
|
});
|
|
11828
12023
|
Schema.Union([GitTarget, RepoNotFound]);
|
|
11829
12024
|
/** A repo whose baseline isn't readable yet: Git is told to try again in a few seconds. */
|
|
@@ -11834,28 +12029,6 @@ const CurrentBaseline = Schema.Struct({
|
|
|
11834
12029
|
name: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/))
|
|
11835
12030
|
});
|
|
11836
12031
|
Schema.Union([CurrentBaseline, RepoNotReady]);
|
|
11837
|
-
//#endregion
|
|
11838
|
-
//#region ../contracts/src/fork-calls.ts
|
|
11839
|
-
/**
|
|
11840
|
-
* A session's fork of a repo: its own copy, made in the background from the repo's current
|
|
11841
|
-
* baseline when the session first reaches the repo. Agents push only to their own session's
|
|
11842
|
-
* forks. A fork isn't deleted yet: when it may be, and what's saved first, waits for the user.
|
|
11843
|
-
*/
|
|
11844
|
-
/**
|
|
11845
|
-
* Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
|
|
11846
|
-
* project's visibility, which a session's routes read who sees the project by.
|
|
11847
|
-
*/
|
|
11848
|
-
const SessionPlace = Schema.Struct({
|
|
11849
|
-
owner: OwnerId,
|
|
11850
|
-
project: ProjectId,
|
|
11851
|
-
visibility: Visibility,
|
|
11852
|
-
thread: ThreadId,
|
|
11853
|
-
threadNumber: ThreadNumber,
|
|
11854
|
-
session: SessionId,
|
|
11855
|
-
number: SessionNumber,
|
|
11856
|
-
/** Whether it's still going: not ended, and its lease not run out. */
|
|
11857
|
-
live: Schema.Boolean
|
|
11858
|
-
});
|
|
11859
12032
|
Schema.Struct({ session: SessionId });
|
|
11860
12033
|
Schema.Union([SessionPlace, SessionNotFound]);
|
|
11861
12034
|
Schema.Struct({
|
|
@@ -15154,17 +15327,21 @@ const browserSession = HttpApiSecurity.apiKey({
|
|
|
15154
15327
|
key: browserSessionCookie
|
|
15155
15328
|
});
|
|
15156
15329
|
/**
|
|
15157
|
-
* Every endpoint's credential, checked before its handler runs: a bearer token,
|
|
15158
|
-
*
|
|
15330
|
+
* Every endpoint's credential, checked before its handler runs: a bearer token, a token bound to
|
|
15331
|
+
* a key with a proof that key signed (`Authorization: DPoP` and one `DPoP` header), or a browser
|
|
15332
|
+
* session's cookie. A request with a cookie and a token is refused before it gets here. A client
|
|
15333
|
+
* that signs proofs may fail before it sends anything, when its key store doesn't sign.
|
|
15159
15334
|
*/
|
|
15160
15335
|
var Authorization = class extends HttpApiMiddleware.Service()("ref0/contracts/Authorization", {
|
|
15161
15336
|
requiredForClient: true,
|
|
15162
15337
|
security: {
|
|
15163
15338
|
bearer: HttpApiSecurity.bearer,
|
|
15339
|
+
dpop: HttpApiSecurity.http({ scheme: "DPoP" }),
|
|
15164
15340
|
browserSession
|
|
15165
15341
|
},
|
|
15166
15342
|
error: [
|
|
15167
15343
|
Unauthenticated,
|
|
15344
|
+
InvalidDpopProof,
|
|
15168
15345
|
SessionHasEnded,
|
|
15169
15346
|
AuthorityUnavailable
|
|
15170
15347
|
]
|
|
@@ -15195,6 +15372,18 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
|
|
|
15195
15372
|
* what to change. Any other part that fails its contract is answered with Effect's empty 400.
|
|
15196
15373
|
*/
|
|
15197
15374
|
var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
|
|
15375
|
+
Context.Service()("ref0/contracts/ProvedKey");
|
|
15376
|
+
/**
|
|
15377
|
+
* A request with no credential, signed by a key it names, such as a machine's ask to connect: one
|
|
15378
|
+
* `DPoP` header, a proof with no `ath`, since there's no token, checked before the request is
|
|
15379
|
+
* read, which says the key that signed it; or no proof, which says none. Every client supplies
|
|
15380
|
+
* it, as it does `Authorization`, so its key store's failure, before anything is sent, is typed:
|
|
15381
|
+
* one with a key store signs, and every other sends no proof.
|
|
15382
|
+
*/
|
|
15383
|
+
var KeyProof = class extends HttpApiMiddleware.Service()("ref0/contracts/KeyProof", {
|
|
15384
|
+
requiredForClient: true,
|
|
15385
|
+
error: InvalidDpopProof
|
|
15386
|
+
}) {};
|
|
15198
15387
|
/**
|
|
15199
15388
|
* An identity token's request whose audience its contract refuses, missing or not an HTTPS
|
|
15200
15389
|
* origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
|
|
@@ -15310,8 +15499,9 @@ var AttachmentsGroup = class extends HttpApiGroup.make("attachments").add(HttpAp
|
|
|
15310
15499
|
})).middleware(Authorization) {};
|
|
15311
15500
|
/**
|
|
15312
15501
|
* Connecting a machine, the machine's side: asking, with no credential, to connect to one owner's
|
|
15313
|
-
* agent, whose terminal shows the code their person types
|
|
15314
|
-
* confirming it saved the grant, or aborting, with its collecting
|
|
15502
|
+
* agent, whose terminal shows the code their person types, signed by the machine key it names;
|
|
15503
|
+
* then collecting what came of it, confirming it saved the grant, or aborting, with its collecting
|
|
15504
|
+
* secret, which needs no proof: a grant it collects is bound to the key its ask named.
|
|
15315
15505
|
*/
|
|
15316
15506
|
var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachments").add(HttpApiEndpoint.post("request", "/v1/attachments/requests", {
|
|
15317
15507
|
headers: idempotency,
|
|
@@ -15321,9 +15511,10 @@ var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachment
|
|
|
15321
15511
|
NotPermitted,
|
|
15322
15512
|
CommandRefused,
|
|
15323
15513
|
SlowDown,
|
|
15514
|
+
InvalidDpopProof,
|
|
15324
15515
|
AuthorityUnavailable
|
|
15325
15516
|
]
|
|
15326
|
-
}).middleware(MachineNameRefusal), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
|
|
15517
|
+
}).middleware(MachineNameRefusal).middleware(KeyProof), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
|
|
15327
15518
|
params: { approval: ApprovalId },
|
|
15328
15519
|
payload: Schema.Struct({ answerCode: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(64))) }),
|
|
15329
15520
|
success: RequestState,
|
|
@@ -18557,10 +18748,12 @@ const followApproval = (client, approval, endpoint) => Effect.gen(function* () {
|
|
|
18557
18748
|
* credential it sends, a bearer token or a browser session.
|
|
18558
18749
|
*/
|
|
18559
18750
|
const ref0Client = (origin) => HttpApiClient.make(Api, origin === void 0 ? {} : { baseUrl: origin });
|
|
18751
|
+
/** No proof of a key for a request that asks for one: a client with no key store's. */
|
|
18752
|
+
const noKeyProof = HttpApiMiddleware.layerClient(KeyProof, ({ request, next }) => next(request));
|
|
18560
18753
|
/** A bearer token: an agent's credential, sent with every request. */
|
|
18561
|
-
const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
|
|
18754
|
+
const bearerCredential = (token) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(HttpClientRequest.bearerToken(request, token))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
|
|
18562
18755
|
/** A browser session: the browser sends its cookie on ref0's own origin, so nothing is added. */
|
|
18563
|
-
const browserCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
|
|
18756
|
+
const browserCredential = Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
|
|
18564
18757
|
/** No credential: a terminal asking its person, which sends nothing of theirs. */
|
|
18565
18758
|
const anonymousCredential = browserCredential;
|
|
18566
18759
|
/** The platform's `fetch`, for a client in a browser or a Worker. */
|
|
@@ -18587,6 +18780,121 @@ const liveMessages = (url, message) => {
|
|
|
18587
18780
|
}), (socket) => Effect.sync(() => socket.close())));
|
|
18588
18781
|
};
|
|
18589
18782
|
//#endregion
|
|
18783
|
+
//#region src/client/proofs.ts
|
|
18784
|
+
/**
|
|
18785
|
+
* How far ref0's clock is ahead of this machine's, in milliseconds, as the `Date` of a refused
|
|
18786
|
+
* proof's answer said: provided once for a process, so every proof it signs after is dated by it.
|
|
18787
|
+
*/
|
|
18788
|
+
var ProofClock = class ProofClock extends Context.Service()("ref0/sdk/ProofClock") {
|
|
18789
|
+
static layer = Layer.effect(ProofClock, Effect.map(Ref.make(0), (offset) => ProofClock.of({ offset })));
|
|
18790
|
+
};
|
|
18791
|
+
/** How far apart the two clocks may be before a refused proof is signed again by ref0's. */
|
|
18792
|
+
const clockSkewMillis = 5e3;
|
|
18793
|
+
const encodeHeader = Schema.encodeSync(Schema.fromJsonString(DpopProofHeader));
|
|
18794
|
+
const encodeClaims = Schema.encodeSync(Schema.fromJsonString(DpopProofClaims));
|
|
18795
|
+
const decodeProofId = Schema.decodeSync(DpopProofId);
|
|
18796
|
+
const utf8 = new TextEncoder();
|
|
18797
|
+
/** A fresh UUIDv4 from the platform's cryptographic source, a proof's `jti`. */
|
|
18798
|
+
const freshProofId = Effect.sync(() => {
|
|
18799
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
18800
|
+
bytes[6] = 64 | (bytes[6] ?? 0) & 15;
|
|
18801
|
+
bytes[8] = 128 | (bytes[8] ?? 0) & 63;
|
|
18802
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
18803
|
+
return decodeProofId(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`);
|
|
18804
|
+
});
|
|
18805
|
+
const decodeThumbprint = Schema.decodeSync(KeyThumbprint);
|
|
18806
|
+
/**
|
|
18807
|
+
* RFC 7638's thumbprint of a P-256 public key: the SHA-256 of its required members, in order, as
|
|
18808
|
+
* JSON, in base64url. What a credential bound to the key names it by, as `cnf.jkt`.
|
|
18809
|
+
*/
|
|
18810
|
+
const keyThumbprint = (jwk) => Effect.promise(async () => decodeThumbprint(Base64Url.encode(new Uint8Array(await crypto.subtle.digest("SHA-256", utf8.encode(`{"crv":"P-256","kty":"EC","x":"${jwk.x}","y":"${jwk.y}"}`))))));
|
|
18811
|
+
/** `ath`: the SHA-256 of the whole token, in base64url. */
|
|
18812
|
+
const tokenHashOf = (token) => Effect.promise(async () => Base64Url.encode(new Uint8Array(await crypto.subtle.digest("SHA-256", utf8.encode(token)))));
|
|
18813
|
+
/**
|
|
18814
|
+
* A proof for one attempt at a request: its method, and its URL as built, with no query or
|
|
18815
|
+
* fragment; dated by this machine's clock, moved by `offset`; with the token's hash, if it's sent
|
|
18816
|
+
* with one; signed by the store.
|
|
18817
|
+
*/
|
|
18818
|
+
const proofFor = Effect.fnUntraced(function* (store, attempt, offset) {
|
|
18819
|
+
const now = (yield* Clock.currentTimeMillis) + offset;
|
|
18820
|
+
const header = encodeHeader({
|
|
18821
|
+
typ: "dpop+jwt",
|
|
18822
|
+
alg: "ES256",
|
|
18823
|
+
jwk: store.publicKey
|
|
18824
|
+
});
|
|
18825
|
+
const signs = {
|
|
18826
|
+
jti: yield* freshProofId,
|
|
18827
|
+
htm: attempt.method,
|
|
18828
|
+
htu: `${attempt.url.origin}${attempt.url.pathname}`,
|
|
18829
|
+
iat: Math.floor(now / 1e3)
|
|
18830
|
+
};
|
|
18831
|
+
if (attempt.token !== void 0) signs.ath = yield* tokenHashOf(attempt.token);
|
|
18832
|
+
const claims = encodeClaims(signs);
|
|
18833
|
+
const signed = `${Base64Url.encode(header)}.${Base64Url.encode(claims)}`;
|
|
18834
|
+
const signature = yield* store.sign(utf8.encode(signed));
|
|
18835
|
+
if (signature.byteLength !== 64) return yield* Effect.die("a key store answers r||s, 64 bytes");
|
|
18836
|
+
return `${signed}.${Base64Url.encode(signature)}`;
|
|
18837
|
+
});
|
|
18838
|
+
/**
|
|
18839
|
+
* How far ref0's clock was from this machine's, moved by `offset`, by a refused proof's answer:
|
|
18840
|
+
* none for any other answer, or one without a `Date`.
|
|
18841
|
+
*/
|
|
18842
|
+
const skewOf = (response, local) => {
|
|
18843
|
+
const challenge = response.headers["www-authenticate"] ?? "";
|
|
18844
|
+
if (response.status !== 401 || !challenge.includes("DPoP error=\"invalid_dpop_proof\"")) return Option.none();
|
|
18845
|
+
return Option.map(DateTime.make(response.headers["date"] ?? ""), (date) => DateTime.toEpochMillis(date) - local);
|
|
18846
|
+
};
|
|
18847
|
+
/**
|
|
18848
|
+
* Sends one request, with its credential if it has one, signing each attempt afresh: once, and
|
|
18849
|
+
* once more, by ref0's clock, after a refused proof whose answer is dated more than five seconds
|
|
18850
|
+
* from this machine's. The process keeps the offset for every proof after.
|
|
18851
|
+
*/
|
|
18852
|
+
const signedAttempts = (credential, offsetRef) => ({ request, next }) => Effect.gen(function* () {
|
|
18853
|
+
const token = credential.token === void 0 ? void 0 : Redacted.value(credential.token);
|
|
18854
|
+
const attempt = {
|
|
18855
|
+
method: request.method,
|
|
18856
|
+
url: new URL(`${credential.origin}${request.url}`),
|
|
18857
|
+
token
|
|
18858
|
+
};
|
|
18859
|
+
const redacted = [...yield* Effect.service(Headers.CurrentRedactedNames), "dpop"];
|
|
18860
|
+
const send = (offset) => proofFor(credential.store, attempt, offset).pipe(Effect.map((proof) => HttpClientRequest.setHeaders(request, token === void 0 ? { dpop: proof } : {
|
|
18861
|
+
authorization: `DPoP ${token}`,
|
|
18862
|
+
dpop: proof
|
|
18863
|
+
})), Effect.flatMap(next), Effect.provideService(Headers.CurrentRedactedNames, redacted));
|
|
18864
|
+
const offset = yield* Ref.get(offsetRef);
|
|
18865
|
+
const answer = yield* send(offset);
|
|
18866
|
+
const skew = skewOf(answer, (yield* Clock.currentTimeMillis) + offset);
|
|
18867
|
+
if (Option.isNone(skew) || Math.abs(skew.value) <= clockSkewMillis) return answer;
|
|
18868
|
+
yield* Effect.ignore(answer.text);
|
|
18869
|
+
yield* Ref.set(offsetRef, offset + skew.value);
|
|
18870
|
+
return yield* send(offset + skew.value);
|
|
18871
|
+
});
|
|
18872
|
+
/**
|
|
18873
|
+
* A token bound to a key, sent as `Authorization: DPoP` with a proof its key store signs afresh
|
|
18874
|
+
* for each attempt. Signing in's own endpoints take no token.
|
|
18875
|
+
*/
|
|
18876
|
+
const dpopCredential = (credential) => Layer.mergeAll(HttpApiMiddleware.layerClient(Authorization, Effect.map(Effect.service(ProofClock), (clock) => signedAttempts(credential, clock.offset))), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)), noKeyProof);
|
|
18877
|
+
/**
|
|
18878
|
+
* ref0's `/v1` as a typed client at a bound credential's origin, sending its token with a proof
|
|
18879
|
+
* its key store signs for each attempt. The client is made at the origin its proofs name, so
|
|
18880
|
+
* neither the token nor a proof is sent anywhere else. It needs the process's `ProofClock`. A
|
|
18881
|
+
* request's trace redacts its proof, but an error it fails with still holds the request: a
|
|
18882
|
+
* program that logs errors adds `dpop` to `Headers.CurrentRedactedNames` at its root.
|
|
18883
|
+
*/
|
|
18884
|
+
const boundClient = (credential) => ref0Client(credential.origin).pipe(Effect.provide(dpopCredential(credential)));
|
|
18885
|
+
/**
|
|
18886
|
+
* A request with no credential, signed by the key it names, such as a machine's ask to connect:
|
|
18887
|
+
* a proof its key store signs for each attempt, with no `ath`, since there's no token. Any other
|
|
18888
|
+
* request is sent with nothing added.
|
|
18889
|
+
*/
|
|
18890
|
+
const keyProof = (signer) => Layer.mergeAll(HttpApiMiddleware.layerClient(KeyProof, Effect.map(Effect.service(ProofClock), (clock) => signedAttempts(signer, clock.offset))), HttpApiMiddleware.layerClient(Authorization, ({ request, next }) => next(request)), HttpApiMiddleware.layerClient(SignedIn, ({ request, next }) => next(request)));
|
|
18891
|
+
/**
|
|
18892
|
+
* ref0's `/v1` as a typed client at a key's origin, with no credential, signing each request that
|
|
18893
|
+
* asks for a proof, such as a machine's ask to connect, with the key its store holds. It needs the
|
|
18894
|
+
* process's `ProofClock`, and its errors still hold their requests, as `boundClient`'s do.
|
|
18895
|
+
*/
|
|
18896
|
+
const provingClient = (signer) => ref0Client(signer.origin).pipe(Effect.provide(keyProof(signer)));
|
|
18897
|
+
//#endregion
|
|
18590
18898
|
//#region src/client/request-id.ts
|
|
18591
18899
|
const decodeRequestId = Schema.decodeUnknownEffect(RequestId);
|
|
18592
18900
|
/**
|
|
@@ -18634,6 +18942,6 @@ const signOut = (origin) => Effect.gen(function* () {
|
|
|
18634
18942
|
yield* client.execute(HttpClientRequest.make(method)(path, { body: HttpBody.jsonUnsafe({}) }));
|
|
18635
18943
|
});
|
|
18636
18944
|
//#endregion
|
|
18637
|
-
export { AgentHandle, AnswerKey, ApiOrigin, AttachmentFailed, AttachmentRequestView, ClientAccess, ClientGranted, ConnectionChanged, ConnectionKind, ConnectionName, ConsentProject, FoldersConnected, LiveTrackName, MachineConnected, OwnerName, OwnerTrackMessage, PasskeyAsserted, PasskeyCreated, PreviewUnavailable, ProjectName, ProjectTrackMessage, RepoName, RepoUrl, SecretHash, SentryOrg, SentryRegion, SentryToken, SignInDestination, ThreadId, ThreadStage, TypedApprovalCode, WorkflowPreview, WrittenProject, anonymousCredential, bearerCredential, browserCredential, commandLine, connectEndpoint, encodeSecret, fetchTransport, followApproval, hookTokenPrefix, hookUrl, isRef0Path, liveMessages, liveSocketUrl, newRequestId, projectParts, ref0Client, repoNameKey, settledOf, signOut, startGitHubSignIn, surfaceOf, threadParts };
|
|
18945
|
+
export { AgentHandle, AnswerKey, ApiOrigin, AttachmentFailed, AttachmentRequestView, ClientAccess, ClientGranted, ConnectionChanged, ConnectionKind, ConnectionName, ConsentProject, FoldersConnected, InvalidDpopProof, KeyStoreUnavailable, LiveTrackName, MachineConnected, OwnerName, OwnerTrackMessage, PasskeyAsserted, PasskeyCreated, PreviewUnavailable, ProjectName, ProjectTrackMessage, ProofClock, ProofOrigin, PublicJwk, RepoName, RepoUrl, SecretHash, SentryOrg, SentryRegion, SentryToken, SignInDestination, ThreadId, ThreadStage, TypedApprovalCode, WorkflowPreview, WrittenProject, anonymousCredential, bearerCredential, boundClient, browserCredential, commandLine, connectEndpoint, encodeSecret, fetchTransport, followApproval, hookTokenPrefix, hookUrl, isRef0Path, keyThumbprint, liveMessages, liveSocketUrl, newRequestId, projectParts, provingClient, ref0Client, repoNameKey, settledOf, signOut, startGitHubSignIn, surfaceOf, threadParts };
|
|
18638
18946
|
|
|
18639
18947
|
//# sourceMappingURL=client.mjs.map
|