@ref0/sdk 0.12.1 → 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/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({ live: Schema.Boolean });
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
- /** One to eight config folders, each with its own grant secret. */
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,
@@ -6658,6 +6988,7 @@ Schema.Struct({
6658
6988
  request: ApprovalId,
6659
6989
  machine: MachineId,
6660
6990
  machineName: MachineName,
6991
+ cnf: Schema.optionalKey(KeyBinding),
6661
6992
  browserSession: TokenId,
6662
6993
  personGeneration: Generation,
6663
6994
  folders: Schema.Array(ConnectFolderCommand).check(Schema.isMinLength(1), Schema.isMaxLength(8), uniqueFolderPaths),
@@ -10218,117 +10549,56 @@ const GrantView = Schema.Struct({
10218
10549
  role: GrantRole,
10219
10550
  grantedBy: ActorRef,
10220
10551
  grantedAt: Schema.DateTimeUtcFromMillis
10221
- });
10222
- /** A page of the grants a reader may see, in the order they were given, and where the next starts. */
10223
- const GrantPage = Schema.Struct({
10224
- grants: Schema.Array(GrantView),
10225
- nextAfter: Schema.optionalKey(EventId)
10226
- });
10227
- Schema.Struct({
10228
- reader: PersonId,
10229
- after: Schema.optionalKey(EventId),
10230
- limit: PageLimit
10231
- });
10232
- /**
10233
- * A grant as its Owner DO holds it: what it's on by the names it keeps, a thread by its scope and
10234
- * number; who holds it, by ID, for the Worker to name.
10235
- */
10236
- const GrantFound = Schema.Struct({
10237
- grant: EventId,
10238
- subject: Schema.Union([
10239
- Schema.Struct({ kind: Schema.Literal("owner") }),
10240
- Schema.Struct({
10241
- kind: Schema.Literal("project"),
10242
- project: ProjectName
10243
- }),
10244
- Schema.Struct({
10245
- kind: Schema.Literal("thread"),
10246
- scope: ThreadScope,
10247
- number: ThreadNumber
10248
- }),
10249
- Schema.Struct({
10250
- kind: Schema.Literal("agent"),
10251
- handle: AgentHandle
10252
- })
10253
- ]),
10254
- principal: Principal,
10255
- role: GrantRole,
10256
- grantedBy: ActorRef,
10257
- grantedAt: Schema.DateTimeUtcFromMillis
10258
- });
10259
- Schema.Struct({
10260
- grants: Schema.Array(GrantFound),
10261
- nextAfter: Schema.optionalKey(EventId)
10262
- });
10263
- Schema.Struct({
10264
- project: ProjectId,
10265
- number: ThreadNumber
10266
- });
10267
- Schema.Union([Schema.Struct({
10268
- found: Schema.Literal(true),
10269
- thread: ThreadId
10270
- }), Schema.Struct({ found: Schema.Literal(false) })]);
10271
- /**
10272
- * ref0's one origin, as a client is given it: `http` or `https`, a host and maybe a port, and no
10273
- * credentials, path, query or fragment. That pattern alone lets through a host or port no URL has,
10274
- * 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
10275
- * it too. It's kept as written, case and any default port too: every path here is appended to it
10276
- * as it is.
10277
- */
10278
- 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"));
10279
- /** Where signing in's own routes live, which Better Auth serves, outside `Api`. */
10280
- const signInBase = "/auth";
10281
- /** Whether a path is signing in's. */
10282
- const isSignInPath = (pathname) => pathname.startsWith(`${signInBase}/`);
10283
- /**
10284
- * A route under `signInBase`, by its method: its `path` from the origin's root, as a request
10285
- * names it, and its `pathUnderBase`, as Better Auth names its endpoints.
10286
- */
10287
- const signInRoute = (method, pathUnderBase) => ({
10288
- method,
10289
- pathUnderBase,
10290
- path: `${signInBase}${pathUnderBase}`
10291
- });
10292
- /** Signing in with GitHub, from ref0's pages: starting it, GitHub's way back, and signing out. */
10293
- const signInRoutes = {
10294
- gitHubSignIn: signInRoute("POST", "/sign-in/social"),
10295
- gitHubCallback: signInRoute("GET", "/callback/github"),
10296
- signOut: signInRoute("POST", "/sign-out")
10297
- };
10298
- /**
10299
- * The OAuth server outside MCP clients use (layer 4, PR 11a): its metadata, authorizing, the
10300
- * token endpoint, registering a public client and revoking a token.
10301
- */
10302
- const oauthRoutes = {
10303
- metadata: signInRoute("GET", "/.well-known/oauth-authorization-server"),
10304
- authorize: signInRoute("GET", "/oauth2/authorize"),
10305
- token: signInRoute("POST", "/oauth2/token"),
10306
- register: signInRoute("POST", "/oauth2/register"),
10307
- revoke: signInRoute("POST", "/oauth2/revoke")
10308
- };
10309
- signInRoute("POST", "/oauth2/consent");
10310
- [...Object.values(signInRoutes), ...Object.values(oauthRoutes)];
10311
- /**
10312
- * Where an outside MCP client looks for `/mcp`'s OAuth (layer 4, PR 11a): RFC 9728's metadata
10313
- * for the resource, and RFC 8414's for the server at `/auth`, each at the origin's root.
10314
- */
10315
- const oauthDiscoveryPaths = ["/.well-known/oauth-protected-resource/mcp", `${oauthRoutes.metadata.pathUnderBase}${signInBase}`];
10316
- /**
10317
- * Where an outside service finds identity tokens' issuer and its public keys, each at the
10318
- * origin's root: the issuer's discovery document, and its JWKS. Only these two: every other path
10319
- * under `/.well-known/` is a page's.
10320
- */
10321
- const identityDiscoveryRoutes = {
10322
- configuration: "/.well-known/openid-configuration",
10323
- keys: "/.well-known/jwks.json"
10324
- };
10325
- /** Whether a path is one of identity tokens' two published documents. */
10326
- 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
+ });
10327
10563
  /**
10328
- * Whether a path is Worker "server"'s to answer, not a page's: the API's, signing in's, a
10329
- * connection's webhook, OAuth's metadata, or identity tokens' published documents.
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.
10330
10566
  */
10331
- const isRef0Path = (pathname) => isSignInPath(pathname) || isHooksPath(pathname) || pathname === "/v1" || pathname.startsWith(`/v1/`) || oauthDiscoveryPaths.includes(pathname) || isIdentityDiscoveryPath(pathname);
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) })]);
10332
10602
  //#endregion
10333
10603
  //#region ../contracts/src/live-calls.ts
10334
10604
  /**
@@ -10977,22 +11247,75 @@ const TurnTokens = Schema.Union([Schema.Struct({
10977
11247
  known: Schema.Literal("read"),
10978
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"))
10979
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
+ });
10980
11299
  /**
10981
- * A turn's facts but its tokens, which `TurnFacts` adds for whoever sends them and `turn.ended`
10982
- * 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.
10983
11302
  */
10984
11303
  const turnFactsFields = {
10985
11304
  /** The turn's model, where the harness said it: Codex in every hook. */
10986
- model: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 128))),
11305
+ model: Schema.optionalKey(Name),
10987
11306
  modelSource: ModelSource,
10988
11307
  /** Tool calls by tool name. */
10989
- tools: Schema.Record(Schema.String.check(Schema.isBetweenLength(1, 128)), Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
11308
+ tools: Schema.Record(Name, Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))).check(Schema.makeFilter((tools) => Object.keys(tools).length <= 100 || "at most 100 tools")),
10990
11309
  /** The files its edits touched, by repo and path, at most 50; `more` counts the rest. */
10991
11310
  addresses: Schema.Array(Schema.Struct({
10992
11311
  repo: RepoName,
10993
11312
  path: RepoPath
10994
11313
  })).check(Schema.isMaxLength(50)),
10995
- 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))
10996
11319
  };
10997
11320
  /**
10998
11321
  * What `ref0 mcp` saw of a turn, sent at its `Stop`s and when the next one starts, and again as
@@ -11004,16 +11327,23 @@ const turnFactsFields = {
11004
11327
  const TurnFacts = Schema.Struct({
11005
11328
  ...turnFactsFields,
11006
11329
  tokens: TurnTokens,
11007
- stoppedAfterMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
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)
11008
11333
  });
11009
11334
  /**
11010
- * A turn's facts as `turn.ended` shows them: its tokens only to an admin of the project's owner
11011
- * and to the session's accountable person. A thread's totals still go to every reader of it, in
11012
- * `ThreadUsage`, so on a thread with one person's sessions they are that person's usage.
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.
11013
11339
  */
11014
11340
  const EndedTurnFacts = Schema.Struct({
11015
11341
  ...turnFactsFields,
11016
- 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))
11017
11347
  });
11018
11348
  /**
11019
11349
  * `POST /v1/sessions/{session}/turns`: a new turn, for the harness's own turn ID, so a retry, or
@@ -11641,151 +11971,6 @@ Schema.Struct({
11641
11971
  name: RepoName
11642
11972
  }))
11643
11973
  });
11644
- /** Where an agent's session asks for an identity token. */
11645
- const identityTokenPath = "/v1/me/identity-token";
11646
- /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
11647
- const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
11648
- /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
11649
- const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
11650
- /**
11651
- * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
11652
- * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
11653
- * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
11654
- */
11655
- const parsesAsWritten = (audience) => {
11656
- const parts = audiencePattern.exec(audience);
11657
- if (parts === null || !URL.canParse(audience)) return false;
11658
- const url = new URL(audience);
11659
- const port = parts[2] ?? "";
11660
- return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
11661
- };
11662
- /**
11663
- * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
11664
- * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
11665
- * case and any port too, so `https://verifier.example:443` is another audience than
11666
- * `https://verifier.example`. ref0 never fetches or resolves it.
11667
- */
11668
- 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"));
11669
- /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
11670
- const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
11671
- /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
11672
- 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"));
11673
- /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
11674
- const IssuedIdentityToken = Schema.Struct({
11675
- token: IdentityToken,
11676
- expiresAt: Schema.DateTimeUtcFromMillis
11677
- });
11678
- /**
11679
- * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
11680
- * token, naming its issuance in ref0's log without naming any row or secret.
11681
- */
11682
- 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"));
11683
- /**
11684
- * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
11685
- * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
11686
- * audience, different at every other, and carrying no time.
11687
- */
11688
- const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
11689
- /** A session, as one audience knows it, made as a person's is. */
11690
- const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
11691
- /** The accountable person, last in a token's chain: only their pairwise ID. */
11692
- const PersonActor = Schema.Struct({ sub: PairwisePersonId });
11693
- /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
11694
- const sessionActing = (act) => Schema.Struct({
11695
- sub: PairwiseSessionId,
11696
- act
11697
- });
11698
- const oneSession = sessionActing(PersonActor);
11699
- const twoSessions = sessionActing(oneSession);
11700
- const threeSessions = sessionActing(twoSessions);
11701
- /**
11702
- * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
11703
- * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
11704
- * allows the agent in `sub`, never an actor.
11705
- */
11706
- const IdentityActor = Schema.Union([
11707
- oneSession,
11708
- twoSessions,
11709
- threeSessions,
11710
- sessionActing(threeSessions)
11711
- ]);
11712
- /** A time in a token, in whole UTC epoch seconds. */
11713
- const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
11714
- Schema.Struct({
11715
- iss: ApiOrigin,
11716
- sub: AgentId,
11717
- aud: IdentityAudience,
11718
- iat: EpochSeconds,
11719
- exp: EpochSeconds,
11720
- jti: IdentityTokenId,
11721
- act: IdentityActor
11722
- }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
11723
- /**
11724
- * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
11725
- * never names two keys.
11726
- */
11727
- const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
11728
- Schema.Struct({
11729
- alg: Schema.Literal("ES256"),
11730
- kid: SigningKeyId,
11731
- typ: Schema.Literal("JWT")
11732
- });
11733
- /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
11734
- const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
11735
- /** One public key in the JWKS: exactly these fields, and never a private one. */
11736
- const PublicSigningKey = Schema.Struct({
11737
- kty: Schema.Literal("EC"),
11738
- crv: Schema.Literal("P-256"),
11739
- x: Coordinate,
11740
- y: Coordinate,
11741
- use: Schema.Literal("sig"),
11742
- alg: Schema.Literal("ES256"),
11743
- kid: SigningKeyId
11744
- });
11745
- Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
11746
- Schema.Struct({
11747
- issuer: ApiOrigin,
11748
- jwks_uri: Schema.String,
11749
- subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
11750
- id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
11751
- claims_supported: Schema.Tuple([
11752
- Schema.Literal("iss"),
11753
- Schema.Literal("sub"),
11754
- Schema.Literal("aud"),
11755
- Schema.Literal("exp"),
11756
- Schema.Literal("iat"),
11757
- Schema.Literal("jti"),
11758
- Schema.Literal("act")
11759
- ])
11760
- });
11761
- /**
11762
- * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
11763
- * `IdentityAudience` says. The refused text is never echoed.
11764
- */
11765
- var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
11766
- /**
11767
- * Less than a whole second is left before the session, a parent or the credential expires, or
11768
- * the token would expire before it's recorded: the next prompt renews or replaces the session.
11769
- */
11770
- var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
11771
- /**
11772
- * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
11773
- * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
11774
- * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
11775
- * received or used it. A failed append discards the token, so every token has its event.
11776
- */
11777
- const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
11778
- session: SessionId,
11779
- agent: AgentRef,
11780
- /** The person whose attachment authorized the session's chain, last in the token's `act`. */
11781
- accountable: PersonId,
11782
- jti: IdentityTokenId,
11783
- audience: IdentityAudience,
11784
- issuer: ApiOrigin,
11785
- keyId: SigningKeyId,
11786
- issuedAt: Schema.DateTimeUtcFromMillis,
11787
- expiresAt: Schema.DateTimeUtcFromMillis
11788
- }));
11789
11974
  Schema.Literals([
11790
11975
  "info/refs",
11791
11976
  "git-upload-pack",
@@ -11813,6 +11998,11 @@ Schema.Union([
11813
11998
  Schema.Struct({
11814
11999
  reader: Reader,
11815
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) })),
11816
12006
  push: Schema.optionalKey(Schema.Struct({
11817
12007
  thread: ThreadId,
11818
12008
  session: SessionId
@@ -11826,7 +12016,9 @@ const GitTarget = Schema.Struct({
11826
12016
  * their sessions' forks, and the repo's `main`. Unset for one that reads the repo's project as
11827
12017
  * a whole.
11828
12018
  */
11829
- 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)
11830
12022
  });
11831
12023
  Schema.Union([GitTarget, RepoNotFound]);
11832
12024
  /** A repo whose baseline isn't readable yet: Git is told to try again in a few seconds. */
@@ -11837,28 +12029,6 @@ const CurrentBaseline = Schema.Struct({
11837
12029
  name: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/))
11838
12030
  });
11839
12031
  Schema.Union([CurrentBaseline, RepoNotReady]);
11840
- //#endregion
11841
- //#region ../contracts/src/fork-calls.ts
11842
- /**
11843
- * A session's fork of a repo: its own copy, made in the background from the repo's current
11844
- * baseline when the session first reaches the repo. Agents push only to their own session's
11845
- * forks. A fork isn't deleted yet: when it may be, and what's saved first, waits for the user.
11846
- */
11847
- /**
11848
- * Where a session is, as a fork's URL names it: its owner, project, thread and numbers. With its
11849
- * project's visibility, which a session's routes read who sees the project by.
11850
- */
11851
- const SessionPlace = Schema.Struct({
11852
- owner: OwnerId,
11853
- project: ProjectId,
11854
- visibility: Visibility,
11855
- thread: ThreadId,
11856
- threadNumber: ThreadNumber,
11857
- session: SessionId,
11858
- number: SessionNumber,
11859
- /** Whether it's still going: not ended, and its lease not run out. */
11860
- live: Schema.Boolean
11861
- });
11862
12032
  Schema.Struct({ session: SessionId });
11863
12033
  Schema.Union([SessionPlace, SessionNotFound]);
11864
12034
  Schema.Struct({
@@ -15157,17 +15327,21 @@ const browserSession = HttpApiSecurity.apiKey({
15157
15327
  key: browserSessionCookie
15158
15328
  });
15159
15329
  /**
15160
- * Every endpoint's credential, checked before its handler runs: a bearer token, or a browser
15161
- * session's cookie. A request with both is refused before it gets here.
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.
15162
15334
  */
15163
15335
  var Authorization = class extends HttpApiMiddleware.Service()("ref0/contracts/Authorization", {
15164
15336
  requiredForClient: true,
15165
15337
  security: {
15166
15338
  bearer: HttpApiSecurity.bearer,
15339
+ dpop: HttpApiSecurity.http({ scheme: "DPoP" }),
15167
15340
  browserSession
15168
15341
  },
15169
15342
  error: [
15170
15343
  Unauthenticated,
15344
+ InvalidDpopProof,
15171
15345
  SessionHasEnded,
15172
15346
  AuthorityUnavailable
15173
15347
  ]
@@ -15198,6 +15372,18 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
15198
15372
  * what to change. Any other part that fails its contract is answered with Effect's empty 400.
15199
15373
  */
15200
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
+ }) {};
15201
15387
  /**
15202
15388
  * An identity token's request whose audience its contract refuses, missing or not an HTTPS
15203
15389
  * origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
@@ -15313,8 +15499,9 @@ var AttachmentsGroup = class extends HttpApiGroup.make("attachments").add(HttpAp
15313
15499
  })).middleware(Authorization) {};
15314
15500
  /**
15315
15501
  * Connecting a machine, the machine's side: asking, with no credential, to connect to one owner's
15316
- * agent, whose terminal shows the code their person types; then collecting what came of it,
15317
- * confirming it saved the grant, or aborting, with its collecting secret.
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.
15318
15505
  */
15319
15506
  var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachments").add(HttpApiEndpoint.post("request", "/v1/attachments/requests", {
15320
15507
  headers: idempotency,
@@ -15324,9 +15511,10 @@ var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachment
15324
15511
  NotPermitted,
15325
15512
  CommandRefused,
15326
15513
  SlowDown,
15514
+ InvalidDpopProof,
15327
15515
  AuthorityUnavailable
15328
15516
  ]
15329
- }).middleware(MachineNameRefusal), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15517
+ }).middleware(MachineNameRefusal).middleware(KeyProof), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
15330
15518
  params: { approval: ApprovalId },
15331
15519
  payload: Schema.Struct({ answerCode: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(64))) }),
15332
15520
  success: RequestState,
@@ -18560,10 +18748,12 @@ const followApproval = (client, approval, endpoint) => Effect.gen(function* () {
18560
18748
  * credential it sends, a bearer token or a browser session.
18561
18749
  */
18562
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));
18563
18753
  /** A bearer token: an agent's credential, sent with every request. */
18564
- 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);
18565
18755
  /** A browser session: the browser sends its cookie on ref0's own origin, so nothing is added. */
18566
- 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);
18567
18757
  /** No credential: a terminal asking its person, which sends nothing of theirs. */
18568
18758
  const anonymousCredential = browserCredential;
18569
18759
  /** The platform's `fetch`, for a client in a browser or a Worker. */
@@ -18590,6 +18780,121 @@ const liveMessages = (url, message) => {
18590
18780
  }), (socket) => Effect.sync(() => socket.close())));
18591
18781
  };
18592
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
18593
18898
  //#region src/client/request-id.ts
18594
18899
  const decodeRequestId = Schema.decodeUnknownEffect(RequestId);
18595
18900
  /**
@@ -18637,6 +18942,6 @@ const signOut = (origin) => Effect.gen(function* () {
18637
18942
  yield* client.execute(HttpClientRequest.make(method)(path, { body: HttpBody.jsonUnsafe({}) }));
18638
18943
  });
18639
18944
  //#endregion
18640
- 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 };
18641
18946
 
18642
18947
  //# sourceMappingURL=client.mjs.map