@alpic-ai/sdk 0.0.0-dev.ge127a8f → 0.0.0-dev.ge249bc3

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 (53) hide show
  1. package/dist/api/auth-scope.d.ts +2 -0
  2. package/dist/api/auth-scope.js +7 -0
  3. package/dist/api/auth-scope.js.map +1 -0
  4. package/dist/api/client.d.ts +5 -1
  5. package/dist/api/client.js +11 -8
  6. package/dist/api/client.js.map +1 -1
  7. package/dist/auth/index.d.ts +8 -3
  8. package/dist/auth/index.js +23 -5
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/auth/oauth-client.d.ts +1 -1
  11. package/dist/auth/oauth-client.js +3 -3
  12. package/dist/auth/oauth-client.js.map +1 -1
  13. package/dist/auth/server/index.js +1 -1
  14. package/dist/auth/server/index.js.map +1 -1
  15. package/dist/client.d.ts +10 -7
  16. package/dist/client.js +16 -20
  17. package/dist/client.js.map +1 -1
  18. package/dist/deploy/archive.d.ts +7 -0
  19. package/dist/deploy/archive.js +54 -0
  20. package/dist/deploy/archive.js.map +1 -0
  21. package/dist/deploy/deployments.d.ts +43 -0
  22. package/dist/deploy/deployments.js +89 -0
  23. package/dist/deploy/deployments.js.map +1 -0
  24. package/dist/deploy/poll.d.ts +2 -0
  25. package/dist/deploy/poll.js +10 -0
  26. package/dist/deploy/poll.js.map +1 -0
  27. package/dist/deploy/upload.d.ts +1 -0
  28. package/dist/deploy/upload.js +13 -0
  29. package/dist/deploy/upload.js.map +1 -0
  30. package/dist/index.d.ts +6 -3
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/emitter.d.ts +3 -3
  34. package/dist/lib/emitter.js +22 -24
  35. package/dist/lib/emitter.js.map +1 -1
  36. package/dist/project/config.d.ts +10 -0
  37. package/dist/project/config.js +29 -0
  38. package/dist/project/config.js.map +1 -0
  39. package/dist/project/detect-runtime.d.ts +2 -0
  40. package/dist/project/detect-runtime.js +25 -0
  41. package/dist/project/detect-runtime.js.map +1 -0
  42. package/dist/project/projects.d.ts +21 -0
  43. package/dist/project/projects.js +52 -0
  44. package/dist/project/projects.js.map +1 -0
  45. package/dist/tunnel/tunnel.d.ts +13 -10
  46. package/dist/tunnel/tunnel.js +122 -47
  47. package/dist/tunnel/tunnel.js.map +1 -1
  48. package/dist/vendor/pipenet.js +47 -41
  49. package/dist/vendor/pipenet.js.map +4 -4
  50. package/dist/whoami.d.ts +15 -0
  51. package/dist/whoami.js +12 -0
  52. package/dist/whoami.js.map +1 -0
  53. package/package.json +6 -5
@@ -1,31 +1,29 @@
1
1
  // Minimal typed emitter for SDK lifecycle hooks. Unlike node's EventEmitter it
2
2
  // never throws on an unlistened "error" event, and swallows listener errors so
3
3
  // observability can never break the operation that emitted the event.
4
- export function createEmitter() {
5
- const listeners = new Map();
6
- return {
7
- on(event, listener) {
8
- const set = listeners.get(event) ?? new Set();
9
- set.add(listener);
10
- listeners.set(event, set);
11
- },
12
- off(event, listener) {
13
- listeners.get(event)?.delete(listener);
14
- },
15
- emit(event, payload) {
16
- const set = listeners.get(event);
17
- if (!set) {
18
- return;
4
+ export class TypedEventEmitter {
5
+ listeners = new Map();
6
+ on(event, listener) {
7
+ const set = this.listeners.get(event) ?? new Set();
8
+ set.add(listener);
9
+ this.listeners.set(event, set);
10
+ }
11
+ off(event, listener) {
12
+ this.listeners.get(event)?.delete(listener);
13
+ }
14
+ emit(event, payload) {
15
+ const set = this.listeners.get(event);
16
+ if (!set) {
17
+ return;
18
+ }
19
+ for (const listener of set) {
20
+ try {
21
+ listener(payload);
19
22
  }
20
- for (const listener of set) {
21
- try {
22
- listener(payload);
23
- }
24
- catch {
25
- // Listener errors must never break the emitting operation.
26
- }
23
+ catch {
24
+ // Listener errors must never break the emitting operation.
27
25
  }
28
- },
29
- };
26
+ }
27
+ }
30
28
  }
31
29
  //# sourceMappingURL=emitter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"emitter.js","sourceRoot":"","sources":["../../src/lib/emitter.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,+EAA+E;AAC/E,sEAAsE;AACtE,MAAM,UAAU,aAAa;IAC3B,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0C,CAAC;IAEpE,OAAO;QACL,EAAE,CAAC,KAAK,EAAE,QAAQ;YAChB,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,EAAqB,CAAC;YACjE,GAAG,CAAC,GAAG,CAAC,QAA6B,CAAC,CAAC;YACvC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC5B,CAAC;QACD,GAAG,CAAC,KAAK,EAAE,QAAQ;YACjB,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAA6B,CAAC,CAAC;QAC9D,CAAC;QACD,IAAI,CAAC,KAAK,EAAE,OAAO;YACjB,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,OAAO;YACT,CAAC;YACD,KAAK,MAAM,QAAQ,IAAI,GAAG,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACH,QAAQ,CAAC,OAAkB,CAAC,CAAC;gBAC/B,CAAC;gBAAC,MAAM,CAAC;oBACP,2DAA2D;gBAC7D,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"emitter.js","sourceRoot":"","sources":["../../src/lib/emitter.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,+EAA+E;AAC/E,sEAAsE;AACtE,MAAM,OAAO,iBAAiB;IACX,SAAS,GAAG,IAAI,GAAG,EAA0C,CAAC;IAE/E,EAAE,CAA2B,KAAQ,EAAE,QAA+B;QACpE,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,EAAqB,CAAC;QACtE,GAAG,CAAC,GAAG,CAAC,QAA6B,CAAC,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,GAAG,CAA2B,KAAQ,EAAE,QAA+B;QACrE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAA6B,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,CAA2B,KAAQ,EAAE,OAAoB;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;QACT,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACF,QAAkC,CAAC,OAAO,CAAC,CAAC;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACP,2DAA2D;YAC7D,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ export type ProjectConfig = {
2
+ projectId: string;
3
+ teamId: string;
4
+ teamName?: string;
5
+ projectName: string;
6
+ environmentId?: string;
7
+ environmentName?: string;
8
+ };
9
+ export declare function loadProjectConfig(): ProjectConfig | null;
10
+ export declare function saveProjectConfig(projectConfig: ProjectConfig): void;
@@ -0,0 +1,29 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const getProjectJsonPath = () => join(process.cwd(), ".alpic", "project.json");
4
+ export function loadProjectConfig() {
5
+ const path = getProjectJsonPath();
6
+ if (!existsSync(path)) {
7
+ return null;
8
+ }
9
+ let raw;
10
+ try {
11
+ raw = JSON.parse(readFileSync(path, "utf8"));
12
+ }
13
+ catch (error) {
14
+ throw new Error(`Failed to load project config: ${error.message}`);
15
+ }
16
+ return {
17
+ projectId: raw.projectId,
18
+ teamId: raw.teamId,
19
+ teamName: raw.teamName,
20
+ projectName: raw.projectName ?? raw.projectId,
21
+ environmentId: raw.environmentId,
22
+ environmentName: raw.environmentName,
23
+ };
24
+ }
25
+ export function saveProjectConfig(projectConfig) {
26
+ mkdirSync(join(process.cwd(), ".alpic"), { recursive: true });
27
+ writeFileSync(getProjectJsonPath(), JSON.stringify(projectConfig, null, 2));
28
+ }
29
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/project/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAWjC,MAAM,kBAAkB,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;AAE/E,MAAM,UAAU,iBAAiB;IAC/B,MAAM,IAAI,GAAG,kBAAkB,EAAE,CAAC;IAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,GAAmE,CAAC;IACxE,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAG1C,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,kCAAmC,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAChF,CAAC;IACD,OAAO;QACL,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,SAAS;QAC7C,aAAa,EAAE,GAAG,CAAC,aAAa;QAChC,eAAe,EAAE,GAAG,CAAC,eAAe;KACrC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,aAA4B;IAC5D,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9D,aAAa,CAAC,kBAAkB,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Runtime } from "@alpic-ai/api";
2
+ export declare function detectRuntime(): Runtime | null;
@@ -0,0 +1,25 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ export function detectRuntime() {
3
+ const has = (file) => existsSync(file);
4
+ const read = (file) => {
5
+ try {
6
+ return readFileSync(file, "utf8");
7
+ }
8
+ catch {
9
+ return "";
10
+ }
11
+ };
12
+ if (has("pyproject.toml") || has("requirements.txt") || has("setup.py") || has(".python-version")) {
13
+ return "python3.13";
14
+ }
15
+ if (has("package.json")) {
16
+ const nvmrc = has(".nvmrc") ? (read(".nvmrc").trim().split(/\s/)[0] ?? "") : "";
17
+ const major = nvmrc.replace(/^v/i, "").match(/^(\d+)/)?.[1];
18
+ if (major === "22") {
19
+ return "node22";
20
+ }
21
+ return "node24";
22
+ }
23
+ return null;
24
+ }
25
+ //# sourceMappingURL=detect-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-runtime.js","sourceRoot":"","sources":["../../src/project/detect-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGnD,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE;QACpC,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAClG,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { RouterInput, Runtime } from "@alpic-ai/api";
2
+ import type { AlpicApi } from "../api/client.js";
3
+ import { type ProjectConfig } from "./config.js";
4
+ export type CreateLinkedProjectOptions = RouterInput["projects"]["create"]["v1"] & {
5
+ teamName?: string;
6
+ };
7
+ export type LinkedProjectConfig = ProjectConfig & {
8
+ environmentId: string;
9
+ };
10
+ export type ProjectsDeps = {
11
+ api: AlpicApi;
12
+ };
13
+ export declare class Projects {
14
+ private readonly api;
15
+ constructor(deps: ProjectsDeps);
16
+ loadConfig(): ProjectConfig | null;
17
+ saveConfig(projectConfig: ProjectConfig): void;
18
+ detectRuntime(): Runtime | null;
19
+ validateLink(projectConfig: ProjectConfig): Promise<ProjectConfig | null>;
20
+ createLinked({ teamName, ...input }: CreateLinkedProjectOptions): Promise<LinkedProjectConfig>;
21
+ }
@@ -0,0 +1,52 @@
1
+ import { ORPCError } from "@orpc/client";
2
+ import { loadProjectConfig, saveProjectConfig } from "./config.js";
3
+ import { detectRuntime } from "./detect-runtime.js";
4
+ export class Projects {
5
+ api;
6
+ constructor(deps) {
7
+ this.api = deps.api;
8
+ }
9
+ loadConfig() {
10
+ return loadProjectConfig();
11
+ }
12
+ saveConfig(projectConfig) {
13
+ saveProjectConfig(projectConfig);
14
+ }
15
+ detectRuntime() {
16
+ return detectRuntime();
17
+ }
18
+ async validateLink(projectConfig) {
19
+ let project;
20
+ try {
21
+ project = await this.api.projects.get.v1({ projectId: projectConfig.projectId });
22
+ }
23
+ catch (error) {
24
+ if (error instanceof ORPCError && error.code === "NOT_FOUND") {
25
+ return null;
26
+ }
27
+ throw error;
28
+ }
29
+ if (projectConfig.environmentId && !project.environments?.some((env) => env.id === projectConfig.environmentId)) {
30
+ return { ...projectConfig, environmentId: undefined, environmentName: undefined };
31
+ }
32
+ return projectConfig;
33
+ }
34
+ async createLinked({ teamName, ...input }) {
35
+ const created = await this.api.projects.create.v1(input);
36
+ const productionEnv = created.productionEnvironment;
37
+ if (!productionEnv) {
38
+ throw new Error("Project was created but has no Production environment.");
39
+ }
40
+ const projectConfig = {
41
+ projectId: created.id,
42
+ teamId: created.teamId,
43
+ teamName,
44
+ projectName: created.name,
45
+ environmentId: productionEnv.id,
46
+ environmentName: productionEnv.name,
47
+ };
48
+ saveProjectConfig(projectConfig);
49
+ return projectConfig;
50
+ }
51
+ }
52
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/project/projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAsB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYpD,MAAM,OAAO,QAAQ;IACF,GAAG,CAAW;IAE/B,YAAY,IAAkB;QAC5B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;IACtB,CAAC;IAED,UAAU;QACR,OAAO,iBAAiB,EAAE,CAAC;IAC7B,CAAC;IAED,UAAU,CAAC,aAA4B;QACrC,iBAAiB,CAAC,aAAa,CAAC,CAAC;IACnC,CAAC;IAED,aAAa;QACX,OAAO,aAAa,EAAE,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,aAA4B;QAC7C,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,aAAa,CAAC,SAAS,EAAE,CAAC,CAAC;QACnF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC7D,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QACD,IAAI,aAAa,CAAC,aAAa,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;YAChH,OAAO,EAAE,GAAG,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC;QACpF,CAAC;QACD,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAA8B;QACnE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QACzD,MAAM,aAAa,GAAG,OAAO,CAAC,qBAAqB,CAAC;QACpD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QACD,MAAM,aAAa,GAAwB;YACzC,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ;YACR,WAAW,EAAE,OAAO,CAAC,IAAI;YACzB,aAAa,EAAE,aAAa,CAAC,EAAE;YAC/B,eAAe,EAAE,aAAa,CAAC,IAAI;SACpC,CAAC;QACF,iBAAiB,CAAC,aAAa,CAAC,CAAC;QACjC,OAAO,aAAa,CAAC;IACvB,CAAC;CACF"}
@@ -3,20 +3,24 @@ export type TunnelRequest = {
3
3
  method: string;
4
4
  path: string;
5
5
  };
6
+ export type TunnelCloseInfo = {
7
+ displaced?: boolean;
8
+ };
6
9
  export type TunnelHandle = {
7
10
  readonly url: string;
8
11
  on(event: "request", listener: (req: TunnelRequest) => void): void;
9
12
  on(event: "error", listener: (err: Error) => void): void;
10
- on(event: "close", listener: () => void): void;
13
+ on(event: "close", listener: (info?: TunnelCloseInfo) => void): void;
11
14
  close(): void;
12
15
  };
13
16
  export type OpenTunnelOptions = {
14
17
  port: number;
15
- autoLogin?: boolean;
16
- onLoginUrl?: (url: string) => void;
17
18
  };
18
19
  type PipenetSocket = {
19
20
  on(event: string, listener: (...args: unknown[]) => void): void;
21
+ tunnelCluster?: {
22
+ on(event: string, listener: (...args: unknown[]) => void): void;
23
+ };
20
24
  close(): void;
21
25
  };
22
26
  type Pipenet = (opts: {
@@ -27,14 +31,13 @@ type Pipenet = (opts: {
27
31
  keepAliveDelayMs: number;
28
32
  offlineTimeoutMs: number;
29
33
  }) => Promise<PipenetSocket>;
30
- export declare function createTunnel(deps: {
34
+ export type TunnelDeps = {
31
35
  api: AlpicApi;
32
- ensureToken: (opts: {
33
- autoLogin?: boolean;
34
- onLoginUrl?: (url: string) => void;
35
- }) => Promise<string>;
36
36
  pipenet?: Pipenet;
37
- }): {
38
- open(opts: OpenTunnelOptions): Promise<TunnelHandle>;
39
37
  };
38
+ export declare class Tunnel {
39
+ private readonly deps;
40
+ constructor(deps: TunnelDeps);
41
+ open(opts: OpenTunnelOptions): Promise<TunnelHandle>;
42
+ }
40
43
  export {};
@@ -1,52 +1,127 @@
1
1
  const CONNECT_TIMEOUT_MS = 10_000;
2
2
  const KEEPALIVE_DELAY_MS = 30_000;
3
3
  const OFFLINE_TIMEOUT_MS = 30_000;
4
- export function createTunnel(deps) {
5
- return {
6
- async open(opts) {
7
- // Load the patched pipenet bundle lazily so non-tunnel consumers (every
8
- // `alpic` command, the skybridge dev server) don't pay its ~500KB load
9
- // cost at startup — only when a tunnel is actually opened.
10
- const pipenet = deps.pipenet ?? (await import("../vendor/pipenet.js")).pipenet;
11
- const accessToken = await deps.ensureToken({
12
- autoLogin: opts.autoLogin,
13
- onLoginUrl: opts.onLoginUrl,
14
- });
15
- const ticket = await deps.api.tunnels.getTicket.v1(undefined, {
16
- context: {
17
- request: { headers: { Authorization: `Bearer ${accessToken}` } },
18
- },
19
- });
20
- const proto = ticket.tunnelHost.startsWith("localhost") ? "http" : "https";
21
- let timeoutId;
22
- const socket = await Promise.race([
23
- pipenet({
24
- port: opts.port,
25
- host: `${proto}://${ticket.tunnelHost}`,
26
- subdomain: ticket.subdomain,
27
- headers: { Authorization: `Bearer ${ticket.ticket}` },
28
- keepAliveDelayMs: KEEPALIVE_DELAY_MS,
29
- offlineTimeoutMs: OFFLINE_TIMEOUT_MS,
30
- }).then((socket) => {
31
- clearTimeout(timeoutId);
32
- return socket;
33
- }),
34
- new Promise((_, reject) => {
35
- timeoutId = setTimeout(() => reject(new Error("Failed to connect to tunnel server")), CONNECT_TIMEOUT_MS);
36
- timeoutId.unref?.();
37
- }),
38
- ]);
39
- const url = `${proto}://${ticket.subdomain}.${ticket.tunnelHost}`;
40
- return {
41
- url,
42
- on(event, listener) {
43
- socket.on(event, listener);
44
- },
45
- close() {
46
- socket.close();
47
- },
48
- };
49
- },
50
- };
4
+ // Takeover detection thresholds — see the "dead" listener in open().
5
+ const FLAP_WINDOW_MS = 5_000;
6
+ const FLAP_MIN_SPAN_MS = 2_000;
7
+ const FLAP_THRESHOLD = 15;
8
+ export class Tunnel {
9
+ deps;
10
+ constructor(deps) {
11
+ this.deps = deps;
12
+ }
13
+ async open(opts) {
14
+ // Load the patched pipenet bundle lazily so non-tunnel consumers (every
15
+ // `alpic` command, the skybridge dev server) don't pay its ~500KB load
16
+ // cost at startup — only when a tunnel is actually opened.
17
+ const pipenet = this.deps.pipenet ?? (await import("../vendor/pipenet.js")).pipenet;
18
+ const ticket = await this.deps.api.tunnels.getTicket.v1();
19
+ // ALPIC_TUNNEL_HOST points a real ticket at a locally-run tunnel server. It's a dev
20
+ // override for a local (plain-HTTP) server, so default to http regardless of the host;
21
+ // pass an explicit https:// to force TLS.
22
+ const hostOverride = process.env.ALPIC_TUNNEL_HOST;
23
+ let tunnelHost;
24
+ let proto;
25
+ if (hostOverride) {
26
+ proto = hostOverride.startsWith("https://") ? "https" : "http";
27
+ tunnelHost = hostOverride.replace(/^https?:\/\//, "");
28
+ }
29
+ else {
30
+ tunnelHost = ticket.tunnelHost;
31
+ proto = tunnelHost.startsWith("localhost") ? "http" : "https";
32
+ }
33
+ let timeoutId;
34
+ const socket = await Promise.race([
35
+ pipenet({
36
+ port: opts.port,
37
+ host: `${proto}://${tunnelHost}`,
38
+ subdomain: ticket.subdomain,
39
+ headers: { Authorization: `Bearer ${ticket.ticket}` },
40
+ keepAliveDelayMs: KEEPALIVE_DELAY_MS,
41
+ offlineTimeoutMs: OFFLINE_TIMEOUT_MS,
42
+ }).then((socket) => {
43
+ clearTimeout(timeoutId);
44
+ return socket;
45
+ }),
46
+ new Promise((_, reject) => {
47
+ timeoutId = setTimeout(() => reject(new Error("Failed to connect to tunnel server")), CONNECT_TIMEOUT_MS);
48
+ timeoutId.unref?.();
49
+ }),
50
+ ]);
51
+ const url = `${proto}://${ticket.subdomain}.${tunnelHost}`;
52
+ const requestListeners = new Set();
53
+ const errorListeners = new Set();
54
+ const closeListeners = new Set();
55
+ let lastRequestAt = 0;
56
+ let displaced = false;
57
+ let closed = false;
58
+ socket.on("request", (req) => {
59
+ lastRequestAt = Date.now();
60
+ for (const listener of requestListeners) {
61
+ listener(req);
62
+ }
63
+ });
64
+ socket.on("error", (err) => {
65
+ for (const listener of errorListeners) {
66
+ listener(err);
67
+ }
68
+ });
69
+ socket.on("close", () => {
70
+ closed = true;
71
+ const info = displaced ? { displaced: true } : undefined;
72
+ for (const listener of closeListeners) {
73
+ listener(info);
74
+ }
75
+ });
76
+ // Detect being displaced by a newer tunnel. When another tunnel takes over this
77
+ // account's subdomain, the server closes our tunnel and rejects our reconnections
78
+ // (they target a session id that no longer exists), so each one connects and is
79
+ // dropped within milliseconds. pipenet's offlineTimeoutMs never fires for this — it
80
+ // only triggers when the server is unreachable entirely (failed connects emit no
81
+ // "dead"). A "dead" with a prior "open" means a socket reached the server and was
82
+ // dropped, so a sustained storm of them is the takeover signature.
83
+ //
84
+ // Normal serving also churns sockets: with the server's small keep-alive pool, most
85
+ // concurrent requests get their socket destroyed right after serving, emitting a
86
+ // "dead" each. A heavy page load can fire 15+ in a few seconds and look identical to
87
+ // a takeover. The discriminator: a real takeover routes all traffic to the new
88
+ // tunnel, so we serve zero requests during the storm. Ignore it while requests flow.
89
+ let drops = [];
90
+ socket.tunnelCluster?.on("dead", () => {
91
+ if (displaced || closed) {
92
+ return;
93
+ }
94
+ const now = Date.now();
95
+ if (now - lastRequestAt < FLAP_WINDOW_MS) {
96
+ drops = [];
97
+ return;
98
+ }
99
+ drops.push(now);
100
+ drops = drops.filter((at) => now - at < FLAP_WINDOW_MS);
101
+ const oldest = drops[0] ?? now;
102
+ if (drops.length >= FLAP_THRESHOLD && now - oldest >= FLAP_MIN_SPAN_MS) {
103
+ displaced = true;
104
+ socket.close();
105
+ }
106
+ });
107
+ return {
108
+ url,
109
+ on(event, listener) {
110
+ if (event === "request") {
111
+ requestListeners.add(listener);
112
+ }
113
+ else if (event === "error") {
114
+ errorListeners.add(listener);
115
+ }
116
+ else {
117
+ closeListeners.add(listener);
118
+ }
119
+ },
120
+ close() {
121
+ closed = true;
122
+ socket.close();
123
+ },
124
+ };
125
+ }
51
126
  }
52
127
  //# sourceMappingURL=tunnel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../../src/tunnel/tunnel.ts"],"names":[],"mappings":"AAEA,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAgClC,MAAM,UAAU,YAAY,CAAC,IAI5B;IACC,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,IAAuB;YAChC,wEAAwE;YACxE,uEAAuE;YACvE,2DAA2D;YAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAK,CAAC,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC,CAAC,OAA8B,CAAC;YAEvG,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC;gBACzC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,UAAU,EAAE,IAAI,CAAC,UAAU;aAC5B,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE;gBAC5D,OAAO,EAAE;oBACP,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE,EAAE;iBACjE;aACF,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YAE3E,IAAI,SAAoD,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;gBAChC,OAAO,CAAC;oBACN,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,IAAI,EAAE,GAAG,KAAK,MAAM,MAAM,CAAC,UAAU,EAAE;oBACvC,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,MAAM,EAAE,EAAE;oBACrD,gBAAgB,EAAE,kBAAkB;oBACpC,gBAAgB,EAAE,kBAAkB;iBACrC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;oBACjB,YAAY,CAAC,SAAS,CAAC,CAAC;oBACxB,OAAO,MAAM,CAAC;gBAChB,CAAC,CAAC;gBACF,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;oBAC/B,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;oBAC1G,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC;gBACtB,CAAC,CAAC;aACH,CAAC,CAAC;YAEH,MAAM,GAAG,GAAG,GAAG,KAAK,MAAM,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YAElE,OAAO;gBACL,GAAG;gBACH,EAAE,CAAC,KAAK,EAAE,QAAQ;oBAChB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,QAAwC,CAAC,CAAC;gBAC7D,CAAC;gBACD,KAAK;oBACH,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../../src/tunnel/tunnel.ts"],"names":[],"mappings":"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"}