@usine/gateway 0.0.0-stage → 0.1.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -2
  3. package/dist/admin/api.d.ts +206 -0
  4. package/dist/admin/api.d.ts.map +1 -0
  5. package/dist/admin/api.js +105 -0
  6. package/dist/admin/api.js.map +1 -0
  7. package/dist/admin/server.d.ts +10 -0
  8. package/dist/admin/server.d.ts.map +1 -0
  9. package/dist/admin/server.js +79 -0
  10. package/dist/admin/server.js.map +1 -0
  11. package/dist/attachment/attachment.d.ts +77 -0
  12. package/dist/attachment/attachment.d.ts.map +1 -0
  13. package/dist/attachment/attachment.js +57 -0
  14. package/dist/attachment/attachment.js.map +1 -0
  15. package/dist/attachment/store.d.ts +27 -0
  16. package/dist/attachment/store.d.ts.map +1 -0
  17. package/dist/attachment/store.js +101 -0
  18. package/dist/attachment/store.js.map +1 -0
  19. package/dist/caller/caller.d.ts +60 -0
  20. package/dist/caller/caller.d.ts.map +1 -0
  21. package/dist/caller/caller.js +130 -0
  22. package/dist/caller/caller.js.map +1 -0
  23. package/dist/egress/client-hello.d.ts +120 -0
  24. package/dist/egress/client-hello.d.ts.map +1 -0
  25. package/dist/egress/client-hello.js +155 -0
  26. package/dist/egress/client-hello.js.map +1 -0
  27. package/dist/egress/egress.d.ts +66 -0
  28. package/dist/egress/egress.d.ts.map +1 -0
  29. package/dist/egress/egress.js +114 -0
  30. package/dist/egress/egress.js.map +1 -0
  31. package/dist/gateway.d.ts +23 -0
  32. package/dist/gateway.d.ts.map +1 -0
  33. package/dist/gateway.js +23 -0
  34. package/dist/gateway.js.map +1 -0
  35. package/dist/github/github.d.ts +43 -0
  36. package/dist/github/github.d.ts.map +1 -0
  37. package/dist/github/github.js +88 -0
  38. package/dist/github/github.js.map +1 -0
  39. package/dist/llm/llm.d.ts +64 -0
  40. package/dist/llm/llm.d.ts.map +1 -0
  41. package/dist/llm/llm.js +92 -0
  42. package/dist/llm/llm.js.map +1 -0
  43. package/dist/node/egress-proxy.d.ts +30 -0
  44. package/dist/node/egress-proxy.d.ts.map +1 -0
  45. package/dist/node/egress-proxy.js +491 -0
  46. package/dist/node/egress-proxy.js.map +1 -0
  47. package/dist/node/environment.d.ts +108 -0
  48. package/dist/node/environment.d.ts.map +1 -0
  49. package/dist/node/environment.js +113 -0
  50. package/dist/node/environment.js.map +1 -0
  51. package/dist/node/main.d.ts +3 -0
  52. package/dist/node/main.d.ts.map +1 -0
  53. package/dist/node/main.js +13 -0
  54. package/dist/node/main.js.map +1 -0
  55. package/dist/node/node.d.ts +29 -0
  56. package/dist/node/node.d.ts.map +1 -0
  57. package/dist/node/node.js +82 -0
  58. package/dist/node/node.js.map +1 -0
  59. package/dist/node/reload.d.ts +26 -0
  60. package/dist/node/reload.d.ts.map +1 -0
  61. package/dist/node/reload.js +142 -0
  62. package/dist/node/reload.js.map +1 -0
  63. package/dist/proxy/forward.d.ts +12 -0
  64. package/dist/proxy/forward.d.ts.map +1 -0
  65. package/dist/proxy/forward.js +44 -0
  66. package/dist/proxy/forward.js.map +1 -0
  67. package/dist/proxy/proxy.d.ts +28 -0
  68. package/dist/proxy/proxy.d.ts.map +1 -0
  69. package/dist/proxy/proxy.js +189 -0
  70. package/dist/proxy/proxy.js.map +1 -0
  71. package/dist/settings.d.ts +52 -0
  72. package/dist/settings.d.ts.map +1 -0
  73. package/dist/settings.js +27 -0
  74. package/dist/settings.js.map +1 -0
  75. package/dist/upstream.d.ts +5 -0
  76. package/dist/upstream.d.ts.map +1 -0
  77. package/dist/upstream.js +6 -0
  78. package/dist/upstream.js.map +1 -0
  79. package/package.json +58 -3
@@ -0,0 +1,77 @@
1
+ import { DateTime, Effect, Schema } from "effect";
2
+ /**
3
+ * The ID of an attachment, unchanged by renewals. A new attach for the same Worker and
4
+ * repository gets a new ID, so an earlier caller token is never accepted again.
5
+ */
6
+ export declare const AttachmentId: Schema.brand<Schema.String, "AttachmentId">;
7
+ export type AttachmentId = typeof AttachmentId.Type;
8
+ /** Returns a new attachment ID of 128 random bits from the platform's cryptographic generator. */
9
+ export declare const newAttachmentId: Effect.Effect<string & import("effect/Brand").Brand<"AttachmentId">, never, never>;
10
+ declare const InstallationCredential_base: Schema.Class<InstallationCredential, Schema.TaggedStruct<"InstallationCredential", {
11
+ readonly token: Schema.Redacted<Schema.NonEmptyString>;
12
+ }>, {}>;
13
+ /** A GitHub App installation token added to the Worker's requests for the repository. */
14
+ export declare class InstallationCredential extends InstallationCredential_base {
15
+ }
16
+ declare const AnonymousCredential_base: Schema.Class<AnonymousCredential, Schema.TaggedStruct<"AnonymousCredential", {}>, {}>;
17
+ /**
18
+ * No credential: requests are forwarded unchanged, so only anonymous GitHub access works, such
19
+ * as reading a public repository.
20
+ */
21
+ export declare class AnonymousCredential extends AnonymousCredential_base {
22
+ }
23
+ /** The credential added to a Worker's GitHub requests for an attached repository. */
24
+ export declare const AttachmentCredential: Schema.Union<readonly [typeof InstallationCredential, typeof AnonymousCredential]>;
25
+ export type AttachmentCredential = typeof AttachmentCredential.Type;
26
+ declare const Attachment_base: Schema.Class<Attachment, Schema.Struct<{
27
+ readonly id: Schema.brand<Schema.String, "AttachmentId">;
28
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
29
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
30
+ readonly credential: Schema.Union<readonly [typeof InstallationCredential, typeof AnonymousCredential]>;
31
+ readonly expiresAt: Schema.DateTimeUtc;
32
+ }>, {}>;
33
+ /**
34
+ * A grant to one Worker VM for one repository. Until `expiresAt`, the Gateway adds `credential`
35
+ * to the VM's requests for that repository and accepts the attachment's caller token.
36
+ * `expiresAt` is the token's expiry. A Worker and repository can have several attachments, one
37
+ * per Attempt.
38
+ */
39
+ export declare class Attachment extends Attachment_base {
40
+ /** Returns whether the attachment grants access at `now`. */
41
+ isLiveAt(now: DateTime.Utc): boolean;
42
+ }
43
+ declare const LiveAttachment_base: Schema.Class<LiveAttachment, Schema.TaggedStruct<"LiveAttachment", {
44
+ readonly attachment: typeof Attachment;
45
+ }>, {}>;
46
+ /** A lookup result for an active attachment. */
47
+ export declare class LiveAttachment extends LiveAttachment_base {
48
+ }
49
+ declare const ExpiredAttachment_base: Schema.Class<ExpiredAttachment, Schema.TaggedStruct<"ExpiredAttachment", {
50
+ readonly expiredAt: Schema.DateTimeUtc;
51
+ }>, {}>;
52
+ /** A lookup result for an attachment whose token expired without renewal. */
53
+ export declare class ExpiredAttachment extends ExpiredAttachment_base {
54
+ }
55
+ declare const MissingAttachment_base: Schema.Class<MissingAttachment, Schema.TaggedStruct<"MissingAttachment", {}>, {}>;
56
+ /** A lookup result for an ID that isn't in the store because it was detached or never attached on this Gateway. */
57
+ export declare class MissingAttachment extends MissingAttachment_base {
58
+ }
59
+ export type AttachmentLookup = LiveAttachment | ExpiredAttachment | MissingAttachment;
60
+ declare const AttachmentAlreadyExpired_base: Schema.Class<AttachmentAlreadyExpired, Schema.TaggedStruct<"AttachmentAlreadyExpired", {
61
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
62
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
63
+ readonly expiresAt: Schema.DateTimeUtc;
64
+ }>, import("effect/Cause").YieldableError>;
65
+ /** The error when an attach request has an expired token. */
66
+ export declare class AttachmentAlreadyExpired extends AttachmentAlreadyExpired_base {
67
+ }
68
+ declare const AttachmentConflict_base: Schema.Class<AttachmentConflict, Schema.TaggedStruct<"AttachmentConflict", {
69
+ readonly id: Schema.brand<Schema.String, "AttachmentId">;
70
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
71
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
72
+ }>, import("effect/Cause").YieldableError>;
73
+ /** The error when a renewal uses an attachment ID that belongs to another Worker or repository. */
74
+ export declare class AttachmentConflict extends AttachmentConflict_base {
75
+ }
76
+ export {};
77
+ //# sourceMappingURL=attachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../../src/attachment/attachment.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAY,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,YAAY,6CAEW,CAAC;AACrC,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAC;AAEpD,kGAAkG;AAClG,eAAO,MAAM,eAAe,oFAE3B,CAAC;;;;AAEF,yFAAyF;AACzF,qBAAa,sBAAuB,SAAQ,2BAG3C;CAAG;;AAEJ;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,wBAGxC;CAAG;AAEJ,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,oFAA8D,CAAC;AAChG,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAC;;;;;;;;AAEpE;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,eAM9B;IACA,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAG,OAAO,CAEnC;CACF;;;;AAED,gDAAgD;AAChD,qBAAa,cAAe,SAAQ,mBAElC;CAAG;;;;AAEL,6EAA6E;AAC7E,qBAAa,iBAAkB,SAAQ,sBAGtC;CAAG;;AAEJ,mHAAmH;AACnH,qBAAa,iBAAkB,SAAQ,sBAGtC;CAAG;AAEJ,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;;;;;;AAEtF,6DAA6D;AAC7D,qBAAa,wBAAyB,SAAQ,6BAG7C;CAAG;;;;;;AAEJ,mGAAmG;AACnG,qBAAa,kBAAmB,SAAQ,uBAGvC;CAAG"}
@@ -0,0 +1,57 @@
1
+ import { RepositoryName } from "@usine/core/repository";
2
+ import { WorkerName } from "@usine/core/worker";
3
+ import { DateTime, Effect, Encoding, Schema } from "effect";
4
+ /**
5
+ * The ID of an attachment, unchanged by renewals. A new attach for the same Worker and
6
+ * repository gets a new ID, so an earlier caller token is never accepted again.
7
+ */
8
+ export const AttachmentId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/u, { message: "Expected a 22-character attachment ID" })).pipe(Schema.brand("AttachmentId"));
9
+ /** Returns a new attachment ID of 128 random bits from the platform's cryptographic generator. */
10
+ export const newAttachmentId = Effect.sync(() => AttachmentId.make(Encoding.encodeBase64Url(crypto.getRandomValues(new Uint8Array(16)))));
11
+ /** A GitHub App installation token added to the Worker's requests for the repository. */
12
+ export class InstallationCredential extends Schema.TaggedClass()("InstallationCredential", { token: Schema.Redacted(Schema.NonEmptyString) }) {
13
+ }
14
+ /**
15
+ * No credential: requests are forwarded unchanged, so only anonymous GitHub access works, such
16
+ * as reading a public repository.
17
+ */
18
+ export class AnonymousCredential extends Schema.TaggedClass()("AnonymousCredential", {}) {
19
+ }
20
+ /** The credential added to a Worker's GitHub requests for an attached repository. */
21
+ export const AttachmentCredential = Schema.Union([InstallationCredential, AnonymousCredential]);
22
+ /**
23
+ * A grant to one Worker VM for one repository. Until `expiresAt`, the Gateway adds `credential`
24
+ * to the VM's requests for that repository and accepts the attachment's caller token.
25
+ * `expiresAt` is the token's expiry. A Worker and repository can have several attachments, one
26
+ * per Attempt.
27
+ */
28
+ export class Attachment extends Schema.Class("@usine/gateway/Attachment")({
29
+ id: AttachmentId,
30
+ worker: WorkerName,
31
+ repository: RepositoryName,
32
+ credential: AttachmentCredential,
33
+ expiresAt: Schema.DateTimeUtc,
34
+ }) {
35
+ /** Returns whether the attachment grants access at `now`. */
36
+ isLiveAt(now) {
37
+ return DateTime.isLessThan(now, this.expiresAt);
38
+ }
39
+ }
40
+ /** A lookup result for an active attachment. */
41
+ export class LiveAttachment extends Schema.TaggedClass()("LiveAttachment", {
42
+ attachment: Attachment,
43
+ }) {
44
+ }
45
+ /** A lookup result for an attachment whose token expired without renewal. */
46
+ export class ExpiredAttachment extends Schema.TaggedClass()("ExpiredAttachment", { expiredAt: Schema.DateTimeUtc }) {
47
+ }
48
+ /** A lookup result for an ID that isn't in the store because it was detached or never attached on this Gateway. */
49
+ export class MissingAttachment extends Schema.TaggedClass()("MissingAttachment", {}) {
50
+ }
51
+ /** The error when an attach request has an expired token. */
52
+ export class AttachmentAlreadyExpired extends Schema.TaggedError()("AttachmentAlreadyExpired", { worker: WorkerName, repository: RepositoryName, expiresAt: Schema.DateTimeUtc }) {
53
+ }
54
+ /** The error when a renewal uses an attachment ID that belongs to another Worker or repository. */
55
+ export class AttachmentConflict extends Schema.TaggedError()("AttachmentConflict", { id: AttachmentId, worker: WorkerName, repository: RepositoryName }) {
56
+ }
57
+ //# sourceMappingURL=attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../src/attachment/attachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE5D;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAC7C,MAAM,CAAC,SAAS,CAAC,sBAAsB,EAAE,EAAE,OAAO,EAAE,uCAAuC,EAAE,CAAC,CAC/F,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;AAGrC,kGAAkG;AAClG,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAC9C,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CACxF,CAAC;AAEF,yFAAyF;AACzF,MAAM,OAAO,sBAAuB,SAAQ,MAAM,CAAC,WAAW,EAA0B,CACtF,wBAAwB,EACxB,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAClD;CAAG;AAEJ;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,MAAM,CAAC,WAAW,EAAuB,CAChF,qBAAqB,EACrB,EAAE,CACH;CAAG;AAEJ,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,sBAAsB,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAGhG;;;;;GAKG;AACH,MAAM,OAAO,UAAW,SAAQ,MAAM,CAAC,KAAK,CAAa,2BAA2B,CAAC,CAAC;IACpF,EAAE,EAAE,YAAY;IAChB,MAAM,EAAE,UAAU;IAClB,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,oBAAoB;IAChC,SAAS,EAAE,MAAM,CAAC,WAAW;CAC9B,CAAC;IACA,6DAA6D;IAC7D,QAAQ,CAAC,GAAiB;QACxB,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAClD,CAAC;CACF;AAED,gDAAgD;AAChD,MAAM,OAAO,cAAe,SAAQ,MAAM,CAAC,WAAW,EAAkB,CAAC,gBAAgB,EAAE;IACzF,UAAU,EAAE,UAAU;CACvB,CAAC;CAAG;AAEL,6EAA6E;AAC7E,MAAM,OAAO,iBAAkB,SAAQ,MAAM,CAAC,WAAW,EAAqB,CAC5E,mBAAmB,EACnB,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,EAAE,CAClC;CAAG;AAEJ,mHAAmH;AACnH,MAAM,OAAO,iBAAkB,SAAQ,MAAM,CAAC,WAAW,EAAqB,CAC5E,mBAAmB,EACnB,EAAE,CACH;CAAG;AAIJ,6DAA6D;AAC7D,MAAM,OAAO,wBAAyB,SAAQ,MAAM,CAAC,WAAW,EAA4B,CAC1F,0BAA0B,EAC1B,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,EAAE,CAClF;CAAG;AAEJ,mGAAmG;AACnG,MAAM,OAAO,kBAAmB,SAAQ,MAAM,CAAC,WAAW,EAAsB,CAC9E,oBAAoB,EACpB,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,CACrE;CAAG"}
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { type Attachment, AttachmentAlreadyExpired, AttachmentConflict, type AttachmentId, type AttachmentLookup } from "./attachment.ts";
3
+ declare const Attachments_base: Context.ServiceClass<Attachments, "@usine/gateway/Attachments", {
4
+ /**
5
+ * Stores an attachment, or replaces the credential and expiry of the one with the same ID.
6
+ * Fails with `AttachmentConflict` if the ID belongs to another Worker or repository.
7
+ */
8
+ readonly attach: (attachment: Attachment) => Effect.Effect<void, AttachmentAlreadyExpired | AttachmentConflict>;
9
+ /** Removes the attachment. Returns `false` if it didn't exist. */
10
+ readonly detach: (id: AttachmentId) => Effect.Effect<boolean>;
11
+ /** Returns the active attachments, earliest expiry first. */
12
+ readonly list: () => Effect.Effect<ReadonlyArray<Attachment>>;
13
+ /** Returns whether the attachment is active, expired, or missing. */
14
+ readonly find: (id: AttachmentId) => Effect.Effect<AttachmentLookup>;
15
+ /** Removes expired attachments and returns the number removed. */
16
+ readonly sweep: () => Effect.Effect<number>;
17
+ }>;
18
+ /**
19
+ * The in-memory attachment store, keyed by attachment ID. A Gateway restart clears it; the
20
+ * executor re-attaches with the same IDs when it renews or restarts.
21
+ */
22
+ export declare class Attachments extends Attachments_base {
23
+ /** An empty store that removes expired attachments every minute while the layer is in scope. */
24
+ static readonly layer: Layer.Layer<Attachments, never, never>;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/attachment/store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAY,MAAM,EAAE,KAAK,EAAO,MAAM,QAAQ,CAAC;AAC/D,OAAO,EACL,KAAK,UAAU,EACf,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAItB,MAAM,iBAAiB,CAAC;;IAerB;;;OAGG;qBACc,CACf,UAAU,EAAE,UAAU,KACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,wBAAwB,GAAG,kBAAkB,CAAC;IACvE,kEAAkE;qBACjD,CAAC,EAAE,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;IAC7D,6DAA6D;mBAC9C,MAAM,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7D,qEAAqE;mBACtD,CAAC,EAAE,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC;IACpE,kEAAkE;oBAClD,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;;AArB/C;;;GAGG;AACH,qBAAa,WAAY,SAAQ,gBAmBA;IAC/B,gGAAgG;IAChG,MAAM,CAAC,QAAQ,CAAC,KAAK,yCAoGnB;CACH"}
@@ -0,0 +1,101 @@
1
+ import { canonicalRepositoryName } from "@usine/core/repository";
2
+ import { Context, DateTime, Effect, Layer, Ref } from "effect";
3
+ import { AttachmentAlreadyExpired, AttachmentConflict, ExpiredAttachment, LiveAttachment, MissingAttachment, } from "./attachment.js";
4
+ const SWEEP_INTERVAL = "1 minute";
5
+ const samePair = (left, right) => left.worker === right.worker &&
6
+ canonicalRepositoryName(left.repository) === canonicalRepositoryName(right.repository);
7
+ /**
8
+ * The in-memory attachment store, keyed by attachment ID. A Gateway restart clears it; the
9
+ * executor re-attaches with the same IDs when it renews or restarts.
10
+ */
11
+ export class Attachments extends Context.Service()("@usine/gateway/Attachments") {
12
+ /** An empty store that removes expired attachments every minute while the layer is in scope. */
13
+ static layer = Layer.effect(Attachments, Effect.gen(function* () {
14
+ const entries = yield* Ref.make(new Map());
15
+ const attach = Effect.fn("Attachments.attach")(function* (attachment) {
16
+ const { id, worker, repository, expiresAt } = attachment;
17
+ yield* Effect.annotateCurrentSpan({ attachment: id, worker, repository });
18
+ const now = yield* DateTime.now;
19
+ if (!attachment.isLiveAt(now))
20
+ return yield* new AttachmentAlreadyExpired({ worker, repository, expiresAt });
21
+ const outcome = yield* Ref.modify(entries, (current) => {
22
+ const previous = current.get(id);
23
+ if (previous !== undefined && !samePair(previous, attachment))
24
+ return ["conflict", current];
25
+ return [
26
+ previous === undefined ? "attached" : "renewed",
27
+ new Map(current).set(id, attachment),
28
+ ];
29
+ });
30
+ if (outcome === "conflict")
31
+ return yield* new AttachmentConflict({ id, worker, repository });
32
+ yield* Effect.logInfo(outcome === "attached" ? "Attached a GitHub credential" : "Renewed a GitHub credential", {
33
+ attachment: id,
34
+ worker,
35
+ repository,
36
+ credential: attachment.credential._tag,
37
+ expiresAt: DateTime.formatIso(expiresAt),
38
+ });
39
+ });
40
+ const detach = Effect.fn("Attachments.detach")(function* (id) {
41
+ yield* Effect.annotateCurrentSpan({ attachment: id });
42
+ const removed = yield* Ref.modify(entries, (current) => {
43
+ const attachment = current.get(id);
44
+ if (attachment === undefined)
45
+ return [attachment, current];
46
+ const next = new Map(current);
47
+ next.delete(id);
48
+ return [attachment, next];
49
+ });
50
+ if (removed === undefined) {
51
+ yield* Effect.logInfo("Nothing to detach", { attachment: id });
52
+ return false;
53
+ }
54
+ yield* Effect.logInfo("Detached a GitHub credential", {
55
+ attachment: id,
56
+ worker: removed.worker,
57
+ repository: removed.repository,
58
+ });
59
+ return true;
60
+ });
61
+ const list = Effect.fn("Attachments.list")(function* () {
62
+ const now = yield* DateTime.now;
63
+ const current = yield* Ref.get(entries);
64
+ return [...current.values()]
65
+ .filter((attachment) => attachment.isLiveAt(now))
66
+ .sort((left, right) => DateTime.toEpochMillis(left.expiresAt) - DateTime.toEpochMillis(right.expiresAt));
67
+ });
68
+ const find = Effect.fn("Attachments.find")(function* (id) {
69
+ const now = yield* DateTime.now;
70
+ const attachment = (yield* Ref.get(entries)).get(id);
71
+ if (attachment === undefined)
72
+ return new MissingAttachment();
73
+ return attachment.isLiveAt(now)
74
+ ? new LiveAttachment({ attachment })
75
+ : new ExpiredAttachment({ expiredAt: attachment.expiresAt });
76
+ });
77
+ const sweep = Effect.fn("Attachments.sweep")(function* () {
78
+ const now = yield* DateTime.now;
79
+ const expired = yield* Ref.modify(entries, (current) => {
80
+ const gone = [...current.values()].filter((attachment) => !attachment.isLiveAt(now));
81
+ if (gone.length === 0)
82
+ return [gone, current];
83
+ const next = new Map(current);
84
+ for (const { id } of gone)
85
+ next.delete(id);
86
+ return [gone, next];
87
+ });
88
+ for (const { id, worker, repository, expiresAt } of expired)
89
+ yield* Effect.logInfo("Forgot an expired GitHub credential", {
90
+ attachment: id,
91
+ worker,
92
+ repository,
93
+ expiredAt: DateTime.formatIso(expiresAt),
94
+ });
95
+ return expired.length;
96
+ });
97
+ yield* Effect.forever(Effect.sleep(SWEEP_INTERVAL).pipe(Effect.andThen(sweep()))).pipe(Effect.forkScoped);
98
+ return { attach, detach, list, find, sweep };
99
+ }));
100
+ }
101
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/attachment/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { Context, Effect, Layer, Option, Schema } from "effect";
2
+ import { Headers } from "effect/unstable/http";
3
+ import { GatewaySettings } from "../settings.ts";
4
+ declare const CallerClaims_base: Schema.Class<CallerClaims, Schema.Struct<{
5
+ readonly attachment: Schema.brand<Schema.String, "AttachmentId">;
6
+ readonly worker: Schema.brand<Schema.String, "WorkerName">;
7
+ readonly repository: Schema.brand<Schema.String, "RepositoryName">;
8
+ }>, {}>;
9
+ /**
10
+ * The claims in a caller token: its attachment ID, Worker, and repository. Because the
11
+ * attachment ID is included, a token from an earlier attachment is rejected after a new attach.
12
+ */
13
+ export declare class CallerClaims extends CallerClaims_base {
14
+ }
15
+ /**
16
+ * The token a Worker VM sends instead of a real credential. It's issued with each attachment
17
+ * and accepted only while that attachment is active.
18
+ */
19
+ export declare const CallerToken: Schema.Redacted<Schema.NonEmptyString>;
20
+ export type CallerToken = typeof CallerToken.Type;
21
+ declare const CallerTokenRejected_base: Schema.Class<CallerTokenRejected, Schema.TaggedStruct<"CallerTokenRejected", {
22
+ readonly reason: Schema.Literals<readonly ["malformed", "bad-signature"]>;
23
+ }>, import("effect/Cause").YieldableError>;
24
+ /** The error for a caller token the Gateway didn't issue: `malformed` if the format is wrong, `bad-signature` if it wasn't signed with the Gateway's key. */
25
+ export declare class CallerTokenRejected extends CallerTokenRejected_base {
26
+ }
27
+ /** The header for tools that can add one, such as git with `http.extraHeader`. */
28
+ export declare const GATEWAY_TOKEN_HEADER = "x-usine-gateway-token";
29
+ declare const CallerTokens_base: Context.ServiceClass<CallerTokens, "@usine/gateway/CallerTokens", {
30
+ readonly issue: (claims: CallerClaims) => Effect.Effect<CallerToken>;
31
+ readonly verify: (token: CallerToken) => Effect.Effect<CallerClaims, CallerTokenRejected>;
32
+ /**
33
+ * Removes caller tokens from headers for a host outside the Gateway: always the Gateway
34
+ * header, and `Authorization` or `x-api-key` only if the value verifies as a caller token.
35
+ */
36
+ readonly withoutCallerTokens: (headers: Headers.Headers) => Effect.Effect<Headers.Headers>;
37
+ }>;
38
+ /**
39
+ * Issues and verifies caller tokens of the form `ugw1.<claims>.<HMAC-SHA256>` under the
40
+ * Gateway's caller key. The same attachment always yields the same token, so no token table
41
+ * is kept.
42
+ */
43
+ export declare class CallerTokens extends CallerTokens_base {
44
+ static readonly layer: Layer.Layer<CallerTokens, never, GatewaySettings>;
45
+ }
46
+ /**
47
+ * Returns the credential in an `Authorization` or `Proxy-Authorization` value: the `Bearer` or
48
+ * `token` parameter, or the `Basic` password (the user name if the password is empty).
49
+ */
50
+ export declare const tokenFromAuthorization: (value: string) => Option.Option<CallerToken>;
51
+ /**
52
+ * Returns the caller token from the request headers, checking the Gateway header, then
53
+ * `Authorization` (gh, Codex, git, and Claude Code with `ANTHROPIC_AUTH_TOKEN` or
54
+ * `CLAUDE_CODE_OAUTH_TOKEN`), then `x-api-key` (Claude Code with `ANTHROPIC_API_KEY`).
55
+ */
56
+ export declare const presentedToken: (headers: Headers.Headers) => Option.Option<CallerToken>;
57
+ /** Returns `headers` without any header that can contain a caller credential. */
58
+ export declare const withoutCallerCredentials: (headers: Headers.Headers) => Headers.Headers;
59
+ export {};
60
+ //# sourceMappingURL=caller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caller.d.ts","sourceRoot":"","sources":["../../src/caller/caller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAY,KAAK,EAAE,MAAM,EAAoB,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;;;;;;AAEjD;;;GAGG;AACH,qBAAa,YAAa,SAAQ,iBAIhC;CAAG;AAEL;;;GAGG;AACH,eAAO,MAAM,WAAW,wCAAyC,CAAC;AAClE,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAC;;;;AAElD,6JAA6J;AAC7J,qBAAa,mBAAoB,SAAQ,wBAGxC;CAAG;AAEJ,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;;oBAgCxC,CAAC,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;qBACnD,CAAC,KAAK,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,mBAAmB,CAAC;IACzF;;;OAGG;kCAC2B,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;;AAd9F;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,iBAWA;IAChC,MAAM,CAAC,QAAQ,CAAC,KAAK,oDAsFnB;CACH;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,UAAW,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,WAAW,CAgB/E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,YAAa,OAAO,CAAC,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,WAAW,CAahF,CAAC;AAEJ,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,YAAa,OAAO,CAAC,OAAO,KAAG,OAAO,CAAC,OACqB,CAAC"}
@@ -0,0 +1,130 @@
1
+ import { RepositoryName } from "@usine/core/repository";
2
+ import { WorkerName } from "@usine/core/worker";
3
+ import { Context, Effect, Encoding, Layer, Option, Redacted, Result, Schema } from "effect";
4
+ import { Headers } from "effect/unstable/http";
5
+ import { AttachmentId } from "../attachment/attachment.js";
6
+ import { GatewaySettings } from "../settings.js";
7
+ /**
8
+ * The claims in a caller token: its attachment ID, Worker, and repository. Because the
9
+ * attachment ID is included, a token from an earlier attachment is rejected after a new attach.
10
+ */
11
+ export class CallerClaims extends Schema.Class("@usine/gateway/CallerClaims")({
12
+ attachment: AttachmentId,
13
+ worker: WorkerName,
14
+ repository: RepositoryName,
15
+ }) {
16
+ }
17
+ /**
18
+ * The token a Worker VM sends instead of a real credential. It's issued with each attachment
19
+ * and accepted only while that attachment is active.
20
+ */
21
+ export const CallerToken = Schema.Redacted(Schema.NonEmptyString);
22
+ /** The error for a caller token the Gateway didn't issue: `malformed` if the format is wrong, `bad-signature` if it wasn't signed with the Gateway's key. */
23
+ export class CallerTokenRejected extends Schema.TaggedError()("CallerTokenRejected", { reason: Schema.Literals(["malformed", "bad-signature"]) }) {
24
+ }
25
+ /** The header for tools that can add one, such as git with `http.extraHeader`. */
26
+ export const GATEWAY_TOKEN_HEADER = "x-usine-gateway-token";
27
+ /** The headers that can contain a caller credential; all are removed before forwarding. */
28
+ const CALLER_CREDENTIAL_HEADERS = [
29
+ GATEWAY_TOKEN_HEADER,
30
+ "authorization",
31
+ "x-api-key",
32
+ "proxy-authorization",
33
+ ];
34
+ const TOKEN_FORMAT = /^ugw1\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/u;
35
+ const encoder = new TextEncoder();
36
+ const toArrayBuffer = (value) => {
37
+ const buffer = new ArrayBuffer(value.byteLength);
38
+ new Uint8Array(buffer).set(value);
39
+ return buffer;
40
+ };
41
+ const ClaimsJson = Schema.fromJsonString(CallerClaims);
42
+ const encodeClaims = Schema.encodeSync(ClaimsJson);
43
+ const decodeClaims = Schema.decodeUnknownOption(ClaimsJson);
44
+ /**
45
+ * Issues and verifies caller tokens of the form `ugw1.<claims>.<HMAC-SHA256>` under the
46
+ * Gateway's caller key. The same attachment always yields the same token, so no token table
47
+ * is kept.
48
+ */
49
+ export class CallerTokens extends Context.Service()("@usine/gateway/CallerTokens") {
50
+ static layer = Layer.effect(CallerTokens, Effect.gen(function* () {
51
+ const { callerKey } = yield* GatewaySettings;
52
+ const key = yield* Effect.promise(() => crypto.subtle.importKey("raw", toArrayBuffer(encoder.encode(Redacted.value(callerKey))), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]));
53
+ const issue = Effect.fn("CallerTokens.issue")(function* (claims) {
54
+ yield* Effect.annotateCurrentSpan({
55
+ attachment: claims.attachment,
56
+ worker: claims.worker,
57
+ repository: claims.repository,
58
+ });
59
+ const signed = `ugw1.${Encoding.encodeBase64Url(encodeClaims(claims))}`;
60
+ const mac = yield* Effect.promise(() => crypto.subtle.sign("HMAC", key, toArrayBuffer(encoder.encode(signed))));
61
+ return Redacted.make(`${signed}.${Encoding.encodeBase64Url(new Uint8Array(mac))}`);
62
+ });
63
+ const verify = Effect.fn("CallerTokens.verify")(function* (token) {
64
+ const match = TOKEN_FORMAT.exec(Redacted.value(token));
65
+ const payload = match?.[1];
66
+ const mac = Option.fromNullishOr(match?.[2]).pipe(Option.flatMap((value) => Result.getSuccess(Encoding.decodeBase64Url(value))));
67
+ if (payload === undefined || Option.isNone(mac))
68
+ return yield* new CallerTokenRejected({ reason: "malformed" });
69
+ const authentic = yield* Effect.promise(() => crypto.subtle.verify("HMAC", key, toArrayBuffer(mac.value), toArrayBuffer(encoder.encode(`ugw1.${payload}`))));
70
+ if (!authentic)
71
+ return yield* new CallerTokenRejected({ reason: "bad-signature" });
72
+ const claims = Result.getSuccess(Encoding.decodeBase64UrlString(payload)).pipe(Option.flatMap(decodeClaims));
73
+ if (Option.isNone(claims))
74
+ return yield* new CallerTokenRejected({ reason: "malformed" });
75
+ yield* Effect.annotateCurrentSpan({
76
+ attachment: claims.value.attachment,
77
+ worker: claims.value.worker,
78
+ repository: claims.value.repository,
79
+ });
80
+ return claims.value;
81
+ });
82
+ const isCallerToken = (token) => Option.match(token, {
83
+ onNone: () => Effect.succeed(false),
84
+ onSome: (candidate) => verify(candidate).pipe(Effect.as(true), Effect.orElseSucceed(() => false)),
85
+ });
86
+ const withoutCallerTokens = Effect.fn("CallerTokens.withoutCallerTokens")(function* (headers) {
87
+ const remaining = Headers.remove(headers, GATEWAY_TOKEN_HEADER);
88
+ const authorization = Headers.get(remaining, "authorization").pipe(Option.flatMap(tokenFromAuthorization));
89
+ const apiKey = Headers.get(remaining, "x-api-key").pipe(Option.map((value) => Redacted.make(value.trim())));
90
+ const withoutAuthorization = (yield* isCallerToken(authorization))
91
+ ? Headers.remove(remaining, "authorization")
92
+ : remaining;
93
+ return (yield* isCallerToken(apiKey))
94
+ ? Headers.remove(withoutAuthorization, "x-api-key")
95
+ : withoutAuthorization;
96
+ });
97
+ return { issue, verify, withoutCallerTokens };
98
+ }));
99
+ }
100
+ /**
101
+ * Returns the credential in an `Authorization` or `Proxy-Authorization` value: the `Bearer` or
102
+ * `token` parameter, or the `Basic` password (the user name if the password is empty).
103
+ */
104
+ export const tokenFromAuthorization = (value) => {
105
+ const match = /^(\S+)\s+(\S+)\s*$/u.exec(value.trim());
106
+ const scheme = match?.[1]?.toLowerCase();
107
+ const parameter = match?.[2];
108
+ if (scheme === undefined || parameter === undefined)
109
+ return Option.none();
110
+ if (scheme === "bearer" || scheme === "token")
111
+ return Option.some(Redacted.make(parameter));
112
+ if (scheme !== "basic")
113
+ return Option.none();
114
+ return Result.getSuccess(Encoding.decodeBase64String(parameter)).pipe(Option.flatMap((pair) => {
115
+ const separator = pair.indexOf(":");
116
+ const user = separator === -1 ? pair : pair.slice(0, separator);
117
+ const password = separator === -1 ? "" : pair.slice(separator + 1);
118
+ const secret = password === "" ? user : password;
119
+ return secret === "" ? Option.none() : Option.some(Redacted.make(secret));
120
+ }));
121
+ };
122
+ /**
123
+ * Returns the caller token from the request headers, checking the Gateway header, then
124
+ * `Authorization` (gh, Codex, git, and Claude Code with `ANTHROPIC_AUTH_TOKEN` or
125
+ * `CLAUDE_CODE_OAUTH_TOKEN`), then `x-api-key` (Claude Code with `ANTHROPIC_API_KEY`).
126
+ */
127
+ export const presentedToken = (headers) => Headers.get(headers, GATEWAY_TOKEN_HEADER).pipe(Option.filter((value) => value.trim() !== ""), Option.map((value) => Redacted.make(value.trim())), Option.orElse(() => Headers.get(headers, "authorization").pipe(Option.flatMap(tokenFromAuthorization))), Option.orElse(() => Headers.get(headers, "x-api-key").pipe(Option.filter((value) => value.trim() !== ""), Option.map((value) => Redacted.make(value.trim())))));
128
+ /** Returns `headers` without any header that can contain a caller credential. */
129
+ export const withoutCallerCredentials = (headers) => CALLER_CREDENTIAL_HEADERS.reduce((remaining, name) => Headers.remove(remaining, name), headers);
130
+ //# sourceMappingURL=caller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caller.js","sourceRoot":"","sources":["../../src/caller/caller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ import { Data, Option } from "effect";
2
+ /** The maximum bytes the forward proxy buffers while waiting for a complete ClientHello. */
3
+ export declare const CLIENT_HELLO_LIMIT: number;
4
+ /**
5
+ * The result of reading a tunnel's first bytes: incomplete with the byte count needed, not a
6
+ * TLS ClientHello, or a ClientHello with its server name and whether it uses ECH.
7
+ */
8
+ export type ClientHelloRead = Data.TaggedEnum<{
9
+ Incomplete: {
10
+ readonly needed: number;
11
+ };
12
+ NotTls: {
13
+ readonly detail: string;
14
+ };
15
+ ClientHello: {
16
+ readonly serverName: Option.Option<string>;
17
+ readonly encryptedClientHello: boolean;
18
+ };
19
+ }>;
20
+ export declare const ClientHelloRead: {
21
+ readonly $is: <Tag extends "ClientHello" | "Incomplete" | "NotTls">(tag: Tag) => (u: unknown) => u is Extract<{
22
+ readonly _tag: "ClientHello";
23
+ readonly serverName: Option.Option<string>;
24
+ readonly encryptedClientHello: boolean;
25
+ }, {
26
+ readonly _tag: Tag;
27
+ }> | Extract<{
28
+ readonly _tag: "Incomplete";
29
+ readonly needed: number;
30
+ }, {
31
+ readonly _tag: Tag;
32
+ }> | Extract<{
33
+ readonly _tag: "NotTls";
34
+ readonly detail: string;
35
+ }, {
36
+ readonly _tag: Tag;
37
+ }>;
38
+ readonly $match: {
39
+ <Cases extends { readonly [Tag in "ClientHello" | "Incomplete" | "NotTls"]: (args: Extract<{
40
+ readonly _tag: "ClientHello";
41
+ readonly serverName: Option.Option<string>;
42
+ readonly encryptedClientHello: boolean;
43
+ } | {
44
+ readonly _tag: "Incomplete";
45
+ readonly needed: number;
46
+ } | {
47
+ readonly _tag: "NotTls";
48
+ readonly detail: string;
49
+ }, {
50
+ readonly _tag: Tag;
51
+ }>) => any; }>(cases: Cases): (value: {
52
+ readonly _tag: "ClientHello";
53
+ readonly serverName: Option.Option<string>;
54
+ readonly encryptedClientHello: boolean;
55
+ } | {
56
+ readonly _tag: "Incomplete";
57
+ readonly needed: number;
58
+ } | {
59
+ readonly _tag: "NotTls";
60
+ readonly detail: string;
61
+ }) => import("effect/Unify").Unify<ReturnType<Cases["ClientHello" | "Incomplete" | "NotTls"]>>;
62
+ <Cases extends { readonly [Tag in "ClientHello" | "Incomplete" | "NotTls"]: (args: Extract<{
63
+ readonly _tag: "ClientHello";
64
+ readonly serverName: Option.Option<string>;
65
+ readonly encryptedClientHello: boolean;
66
+ } | {
67
+ readonly _tag: "Incomplete";
68
+ readonly needed: number;
69
+ } | {
70
+ readonly _tag: "NotTls";
71
+ readonly detail: string;
72
+ }, {
73
+ readonly _tag: Tag;
74
+ }>) => any; }>(value: {
75
+ readonly _tag: "ClientHello";
76
+ readonly serverName: Option.Option<string>;
77
+ readonly encryptedClientHello: boolean;
78
+ } | {
79
+ readonly _tag: "Incomplete";
80
+ readonly needed: number;
81
+ } | {
82
+ readonly _tag: "NotTls";
83
+ readonly detail: string;
84
+ }, cases: Cases): import("effect/Unify").Unify<ReturnType<Cases["ClientHello" | "Incomplete" | "NotTls"]>>;
85
+ };
86
+ readonly ClientHello: Data.TaggedEnum.ConstructorFrom<{
87
+ readonly _tag: "ClientHello";
88
+ readonly serverName: Option.Option<string>;
89
+ readonly encryptedClientHello: boolean;
90
+ }, "_tag">;
91
+ readonly Incomplete: Data.TaggedEnum.ConstructorFrom<{
92
+ readonly _tag: "Incomplete";
93
+ readonly needed: number;
94
+ }, "_tag">;
95
+ readonly NotTls: Data.TaggedEnum.ConstructorFrom<{
96
+ readonly _tag: "NotTls";
97
+ readonly detail: string;
98
+ }, "_tag">;
99
+ };
100
+ /**
101
+ * Reads the TLS ClientHello at the start of `bytes`, joining handshake records if the client
102
+ * split it. For `Incomplete`, `needed` is the smallest byte count that can change the result.
103
+ */
104
+ export declare const readClientHello: (bytes: Uint8Array) => ClientHelloRead;
105
+ /**
106
+ * A buffer for a tunnel's first bytes that parses only when the byte count the last parse
107
+ * needed is reached. A ClientHello longer than 16 KiB is read as `NotTls`.
108
+ */
109
+ export declare class ClientHelloReader {
110
+ #private;
111
+ /** Adds bytes from the client and returns the current read result. */
112
+ push(chunk: Uint8Array): ClientHelloRead;
113
+ /** All bytes collected so far, in order; the proxy writes them upstream once the tunnel opens. */
114
+ get bytes(): Uint8Array;
115
+ /** The number of parses so far. */
116
+ get parses(): number;
117
+ }
118
+ /** Returns `host` lowercased and without a trailing dot, for comparing TLS and CONNECT host names. */
119
+ export declare const comparableHost: (host: string) => string;
120
+ //# sourceMappingURL=client-hello.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-hello.d.ts","sourceRoot":"","sources":["../../src/egress/client-hello.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEtC,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAS5C;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC;IAC5C,UAAU,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,WAAW,EAAE;QACX,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;KACxC,CAAC;CACH,CAAC,CAAC;AACH,eAAO,MAAM,eAAe;;;6BAJH,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;uCACX,OAAO;;;;;yBAJT,MAAM;;;;;yBACV,MAAM;;;;;;;iCAEV,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;2CACX,OAAO;;;6BAJT,MAAM;;;6BACV,MAAM;;;;;;;;;;;;;;;;iCAEV,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;2CACX,OAAO;;;6BAJT,MAAM;;;6BACV,MAAM;;;;;;;;;;;;;;;;;6BAEV,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;uCACX,OAAO;;;;yBAJT,MAAM;;;;yBACV,MAAM;;CAM8B,CAAC;AAsDlE;;;GAGG;AACH,eAAO,MAAM,eAAe,UAAW,UAAU,KAAG,eA+BnD,CAAC;AAEF;;;GAGG;AACH,qBAAa,iBAAiB;;IAM5B,sEAAsE;IACtE,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAUvC;IAED,kGAAkG;IAClG,IAAI,KAAK,IAAI,UAAU,CAWtB;IAED,mCAAmC;IACnC,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF;AAED,sGAAsG;AACtG,eAAO,MAAM,cAAc,SAAU,MAAM,KAAG,MAAgD,CAAC"}