diff --git a/.oxlintrc.json b/.oxlintrc.json index 603eac3..e27804b 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -9,7 +9,7 @@ "!scratchpad/**/*", // TODO: This should be in a nested .oxlintrc.json, but it does not work // See related issue: https://github.com/oxc-project/oxc/issues/13204 - "packages/amp/src/Protobuf/**/*" + "packages/amp/src/protobuf/**/*" ], "jsPlugins": [ "@amp/oxc/oxlint" diff --git a/flake.nix b/flake.nix index 351033a..d0fe31b 100644 --- a/flake.nix +++ b/flake.nix @@ -16,11 +16,18 @@ devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = with pkgs; [ + # JavaScript / TypeScript bun deno corepack nodejs_24 python3 + + # Justfile + just + + # Blockchain + foundry ]; }; }); diff --git a/package.json b/package.json index 4f4259f..f96f7cf 100644 --- a/package.json +++ b/package.json @@ -29,20 +29,20 @@ "@effect/platform": "^0.94.2", "@effect/platform-node": "^0.104.1", "@effect/vitest": "^0.27.0", - "@types/node": "^25.0.10", - "@typescript/native-preview": "7.0.0-dev.20260122.4", + "@types/node": "^25.1.0", + "@typescript/native-preview": "7.0.0-dev.20260130.1", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "babel-plugin-annotate-pure-calls": "^0.5.0", "dprint": "^0.51.1", "effect": "^3.19.15", "glob": "^13.0.0", - "globals": "^17.1.0", + "globals": "^17.2.0", "madge": "^8.0.0", - "oxlint": "^1.41.0", + "oxlint": "^1.42.0", "ts-patch": "^3.3.0", "typescript": "^5.9.3", - "vite-tsconfig-paths": "^6.0.4", + "vite-tsconfig-paths": "^6.0.5", "vitest": "^4.0.18", "vitest-mock-express": "^2.2.0" } diff --git a/packages/amp/buf.gen.yaml b/packages/amp/buf.gen.yaml index 01f306f..fc718de 100644 --- a/packages/amp/buf.gen.yaml +++ b/packages/amp/buf.gen.yaml @@ -6,7 +6,7 @@ inputs: plugins: - local: protoc-gen-es - out: src/Protobuf + out: src/protobuf opt: - target=ts - import_extension=ts diff --git a/packages/amp/package.json b/packages/amp/package.json index af59a46..17f891b 100644 --- a/packages/amp/package.json +++ b/packages/amp/package.json @@ -42,11 +42,11 @@ "coverage": "vitest --coverage" }, "peerDependencies": { - "@bufbuild/protobuf": "^2.10.1", + "@bufbuild/protobuf": "^2.11.0", "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-node": "^2.1.1", - "@effect/platform": "^0.94.0", - "effect": "^3.19.11" + "@effect/platform": "^0.94.2", + "effect": "^3.19.15" }, "devDependencies": { "@bufbuild/buf": "^1.64.0", @@ -59,6 +59,7 @@ }, "dependencies": { "jiti": "^2.6.1", - "viem": "^2.44.4" + "jose": "^6.1.3", + "viem": "^2.45.1" } } diff --git a/packages/amp/src/admin/api.ts b/packages/amp/src/admin/api.ts index 4d3bcc5..fa25b03 100644 --- a/packages/amp/src/admin/api.ts +++ b/packages/amp/src/admin/api.ts @@ -15,7 +15,7 @@ import * as HttpApiError from "@effect/platform/HttpApiError" import * as HttpApiGroup from "@effect/platform/HttpApiGroup" import * as HttpApiSchema from "@effect/platform/HttpApiSchema" import * as Schema from "effect/Schema" -import * as Models from "../Models.ts" +import * as Models from "../models.ts" import * as Domain from "./domain.ts" import * as Error from "./error.ts" diff --git a/packages/amp/src/admin/domain.ts b/packages/amp/src/admin/domain.ts index 1d6ef22..c7516bb 100644 --- a/packages/amp/src/admin/domain.ts +++ b/packages/amp/src/admin/domain.ts @@ -3,7 +3,7 @@ * and responses. */ import * as Schema from "effect/Schema" -import * as Models from "../Models.ts" +import * as Models from "../models.ts" // ============================================================================= // Dataset Request/Response Schemas diff --git a/packages/amp/src/admin/service.ts b/packages/amp/src/admin/service.ts index bc94bd3..a15d4a9 100644 --- a/packages/amp/src/admin/service.ts +++ b/packages/amp/src/admin/service.ts @@ -20,8 +20,8 @@ import * as Effect from "effect/Effect" import { constUndefined } from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" -import * as Auth from "../Auth.ts" -import type * as Models from "../Models.ts" +import * as Auth from "../auth/service.ts" +import type * as Models from "../models.ts" import * as Api from "./api.ts" import type * as Domain from "./domain.ts" @@ -263,7 +263,10 @@ const make = Effect.fnUntraced(function*(options: MakeOptions) { onSome: (auth) => HttpClient.mapRequestEffect( Effect.fnUntraced(function*(request) { - const authInfo = yield* auth.getCachedAuthInfo + const authInfo = yield* auth.getCachedAuthInfo.pipe( + // Treat cache errors as "no auth available" + Effect.catchAll(() => Effect.succeed(Option.none())) + ) if (Option.isNone(authInfo)) return request const token = authInfo.value.accessToken return HttpClientRequest.bearerToken(request, token) diff --git a/packages/amp/src/ArrowFlight.ts b/packages/amp/src/arrow-flight.ts similarity index 98% rename from packages/amp/src/ArrowFlight.ts rename to packages/amp/src/arrow-flight.ts index cfb09ff..54d85fe 100644 --- a/packages/amp/src/ArrowFlight.ts +++ b/packages/amp/src/arrow-flight.ts @@ -19,15 +19,15 @@ import * as Predicate from "effect/Predicate" import * as Redacted from "effect/Redacted" import * as Schema from "effect/Schema" import * as Stream from "effect/Stream" -import { Auth } from "./Auth.ts" +import { Auth } from "./auth/service.ts" import { decodeRecordBatch, DictionaryRegistry } from "./internal/arrow-flight-ipc/Decoder.ts" import { recordBatchToJson } from "./internal/arrow-flight-ipc/Json.ts" import { parseRecordBatch } from "./internal/arrow-flight-ipc/RecordBatch.ts" import { type ArrowSchema, getMessageType, MessageHeaderType, parseSchema } from "./internal/arrow-flight-ipc/Schema.ts" -import type { AuthInfo, BlockRange, RecordBatchMetadata } from "./Models.ts" -import { RecordBatchMetadataFromUint8Array } from "./Models.ts" -import { FlightDescriptor_DescriptorType, FlightDescriptorSchema, FlightService } from "./Protobuf/Flight_pb.ts" -import { CommandStatementQuerySchema } from "./Protobuf/FlightSql_pb.ts" +import type { AuthInfo, BlockRange, RecordBatchMetadata } from "./models.ts" +import { RecordBatchMetadataFromUint8Array } from "./models.ts" +import { FlightDescriptor_DescriptorType, FlightDescriptorSchema, FlightService } from "./protobuf/Flight_pb.ts" +import { CommandStatementQuerySchema } from "./protobuf/FlightSql_pb.ts" // ============================================================================= // Connect RPC Transport @@ -212,7 +212,7 @@ export class ParseSchemaError extends Schema.TaggedError( * is successfully executed. */ export interface QueryResult { - readonly data: A + readonly data: ReadonlyArray readonly metadata: RecordBatchMetadata } @@ -239,7 +239,7 @@ export interface QueryOptions { export type ExtractQueryResult = Options extends { readonly schema: Schema.Schema } ? QueryResult<_A> - : Record + : QueryResult> // ============================================================================= // Arrow Flight Service diff --git a/packages/amp/src/ArrowFlight/Node.ts b/packages/amp/src/arrow-flight/node.ts similarity index 90% rename from packages/amp/src/ArrowFlight/Node.ts rename to packages/amp/src/arrow-flight/node.ts index 08af253..2379ab3 100644 --- a/packages/amp/src/ArrowFlight/Node.ts +++ b/packages/amp/src/arrow-flight/node.ts @@ -1,7 +1,7 @@ import { createGrpcTransport, type GrpcTransportOptions } from "@connectrpc/connect-node" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" -import { Interceptors, Transport } from "../ArrowFlight.ts" +import { Interceptors, Transport } from "../arrow-flight.ts" /** * Create a `Transport` for the gRPC protocol using the Node.js `http2` module. diff --git a/packages/amp/src/auth/error.ts b/packages/amp/src/auth/error.ts new file mode 100644 index 0000000..ced2d74 --- /dev/null +++ b/packages/amp/src/auth/error.ts @@ -0,0 +1,259 @@ +import * as Duration from "effect/Duration" +import * as Schema from "effect/Schema" + +// ============================================================================= +// Helpers +// ============================================================================= + +const AuthErrorCode = (code: Code) => + Schema.Literal(code).pipe( + Schema.propertySignature, + Schema.fromKey("error_code"), + Schema.withConstructorDefault(() => code) + ) + +const BaseAuthErrorFields = { + message: Schema.String.pipe( + Schema.propertySignature, + Schema.fromKey("error_message") + ) +} + +// ============================================================================= +// Errors +// ============================================================================= + +/** + * Indicates that the user's session has expired and they need to re-authenticate. + */ +export class AuthTokenExpiredError extends Schema.TaggedError( + "Amp/Auth/AuthTokenExpiredError" +)("AuthTokenExpiredError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_TOKEN_EXPIRED") +}) { + get userMessage(): string { + return "Your session has expired" + } + get userSuggestion(): string { + return "Run 'amp auth login' to sign in again" + } +} + +/** + * Indicates that too many authentication requests have been made. + */ +export class AuthRateLimitError extends Schema.TaggedError( + "Amp/Auth/AuthRateLimitError" +)("AuthRateLimitError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_RATE_LIMITED"), + retryAfter: Schema.DurationFromMillis +}) { + get userMessage(): string { + return "Too many authentication requests" + } + get userSuggestion(): string { + const duration = Duration.format(this.retryAfter) + return `Please wait about ${duration} before trying again` + } +} + +/** + * Indicates a general token refresh failure. + */ +export class AuthRefreshError extends Schema.TaggedError( + "Amp/Auth/AuthRefreshError" +)("AuthRefreshError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_REFRESH_FAILED"), + status: Schema.optionalWith(Schema.Int, { as: "Option" }), + cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) +}) { + get userMessage(): string { + return "Failed to refresh your authentication token" + } + get userSuggestion(): string { + return "Try signing out and signing in again with 'amp auth logout' then 'amp auth login'" + } +} + +/** + * Indicates that the token belongs to a different user than expected. + */ +export class AuthUserMismatchError extends Schema.TaggedError( + "Amp/Auth/AuthUserMismatchError" +)("AuthUserMismatchError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_USER_MISMATCH"), + expectedUserId: Schema.String, + receivedUserId: Schema.String +}) { + get userMessage(): string { + return "Authentication identity mismatch detected" + } + get userSuggestion(): string { + return "Your cached credentials may be corrupted. Run 'amp auth logout' and 'amp auth login' to re-authenticate" + } +} + +const DeviceFlowReason = Schema.Literal("expired", "pending", "access_denied", "slow_down") +export type DeviceFlowReason = Schema.Schema.Type + +/** + * Indicates an issue with the device authorization flow. + */ +export class AuthDeviceFlowError extends Schema.TaggedError( + "Amp/Auth/AuthDeviceFlowError" +)("AuthDeviceFlowError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_DEVICE_FLOW_ERROR"), + reason: DeviceFlowReason, + verificationUri: Schema.optionalWith(Schema.String, { as: "Option" }) +}) { + get userMessage(): string { + switch (this.reason) { + case "expired": + return "The login code has expired" + case "pending": + return "Waiting for authorization to complete" + case "access_denied": + return "Authorization was denied" + case "slow_down": + return "Too many login attempts" + } + } + get userSuggestion(): string { + switch (this.reason) { + case "expired": + return "Run 'amp auth login' to start a new login session" + case "pending": + return "Complete the login in your browser" + case "access_denied": + return "Run 'amp auth login' to try again" + case "slow_down": + return "Please wait a moment before trying again" + } + } +} + +const CacheOperation = Schema.Literal("read", "write", "clear") +export type CacheOperation = Schema.Schema.Type + +/** + * Indicates a failure with cache read/write/clear operations. + */ +export class AuthCacheError extends Schema.TaggedError( + "Amp/Auth/AuthCacheError" +)("AuthCacheError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_CACHE_ERROR"), + operation: CacheOperation, + cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) +}) { + get userMessage(): string { + switch (this.operation) { + case "read": + return "Could not read saved credentials" + case "write": + return "Could not save your credentials" + case "clear": + return "Could not clear saved credentials" + } + } + get userSuggestion(): string { + return this.operation === "clear" + ? "You may need to manually remove the credentials file" + : "Check file permissions in your configuration directory and try again" + } +} + +/** + * Indicates network or timeout issues during authentication. + */ +export class AuthNetworkError extends Schema.TaggedError( + "Amp/Auth/AuthNetworkError" +)("AuthNetworkError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_NETWORK_ERROR"), + endpoint: Schema.optionalWith(Schema.String, { as: "Option" }), + isTimeout: Schema.Boolean, + cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) +}) { + get userMessage(): string { + return this.isTimeout + ? "Authentication request timed out" + : "Could not connect to the authentication service" + } + get userSuggestion(): string { + return this.isTimeout + ? "The service may be experiencing high load. Please try again in a few moments" + : "Check your internet connection and try again" + } +} + +const VerifyTokenFailureReason = Schema.Literal( + "expired", + "invalid_signature", + "invalid_claims", + "jwks_error", + "unknown" +) +export type VerifyTokenFailureReason = Schema.Schema.Type + +/** + * Indicates a failure when verifying a JWT access token. + */ +export class AuthVerifyTokenError extends Schema.TaggedError( + "Amp/Auth/AuthVerifyTokenError" +)("AuthVerifyTokenError", { + ...BaseAuthErrorFields, + code: AuthErrorCode("AUTH_VERIFY_TOKEN_FAILED"), + reason: VerifyTokenFailureReason, + claim: Schema.optionalWith(Schema.String, { as: "Option" }), + cause: Schema.optionalWith(Schema.Defect, { as: "Option" }) +}) { + get userMessage(): string { + switch (this.reason) { + case "expired": + return "The access token has expired" + case "invalid_signature": + return "The access token signature is invalid" + case "invalid_claims": + return "The access token claims are invalid" + case "jwks_error": + return "Could not verify the access token" + case "unknown": + return "Token verification failed" + } + } + get userSuggestion(): string { + switch (this.reason) { + case "expired": + case "invalid_signature": + case "invalid_claims": + return "Run 'amp auth login' to obtain a new token" + case "jwks_error": + return "Check your internet connection and try again" + case "unknown": + return "Try signing out and signing in again" + } + } +} + +// ============================================================================= +// Union Type +// ============================================================================= + +/** + * A union of all authentication errors for exhaustive handling. + */ +export type AuthError = + | AuthTokenExpiredError + | AuthRateLimitError + | AuthRefreshError + | AuthUserMismatchError + | AuthDeviceFlowError + | AuthCacheError + | AuthNetworkError + | AuthVerifyTokenError diff --git a/packages/amp/src/Auth.ts b/packages/amp/src/auth/service.ts similarity index 50% rename from packages/amp/src/Auth.ts rename to packages/amp/src/auth/service.ts index 5a9855a..48a71b2 100644 --- a/packages/amp/src/Auth.ts +++ b/packages/amp/src/auth/service.ts @@ -1,4 +1,3 @@ -import type * as PlatformError from "@effect/platform/Error" import * as HttpBody from "@effect/platform/HttpBody" import * as HttpClient from "@effect/platform/HttpClient" import type * as HttpClientError from "@effect/platform/HttpClientError" @@ -6,10 +5,10 @@ import * as HttpClientRequest from "@effect/platform/HttpClientRequest" import * as HttpClientResponse from "@effect/platform/HttpClientResponse" import * as KeyValueStore from "@effect/platform/KeyValueStore" import * as UrlParams from "@effect/platform/UrlParams" -import type { TimeoutException } from "effect/Cause" import * as Clock from "effect/Clock" import * as Context from "effect/Context" import * as DateTime from "effect/DateTime" +import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Option from "effect/Option" @@ -17,8 +16,19 @@ import type * as ParseResult from "effect/ParseResult" import * as Predicate from "effect/Predicate" import * as Redacted from "effect/Redacted" import * as Schema from "effect/Schema" -import { pkceChallenge } from "./internal/pkce.ts" -import { AccessToken, Address, AuthInfo, RefreshToken, UserId } from "./Models.ts" +import * as Jose from "jose" +import { pkceChallenge } from "../internal/pkce.ts" +import { AccessToken, Address, AuthInfo, RefreshToken, TokenDuration, UserId } from "../models.ts" +import { + AuthCacheError, + AuthDeviceFlowError, + AuthNetworkError, + AuthRateLimitError, + AuthRefreshError, + AuthTokenExpiredError, + AuthUserMismatchError, + AuthVerifyTokenError +} from "./error.ts" const AUTH_INFO_CACHE_KEY = "amp_cli_auth" export const AUTH_PLATFORM_BASE_URL = new URL("https://auth.amp.thegraph.com/") @@ -137,6 +147,23 @@ export const DeviceTokenPollingResponse = Schema.Union( ) export type DeviceTokenPollingResponse = typeof DeviceTokenPollingResponse.Type +export class GenerateTokenRequest extends Schema.Class( + "Amp/Auth/GenerateTokenRequest" +)({ + audience: Schema.optional(Schema.Array(Schema.String)), + duration: Schema.optional(TokenDuration) +}) {} + +export class GenerateTokenResponse extends Schema.Class( + "Amp/Auth/GenerateTokenResponse" +)({ + token: AccessToken, + token_type: Schema.Literal("Bearer"), + exp: Schema.Int.pipe(Schema.positive()), + sub: Schema.NonEmptyTrimmedString, + iss: Schema.String +}) {} + export class RefreshTokenRequest extends Schema.Class( "Amp/Auth/RefreshTokenRequest" )({ @@ -184,46 +211,13 @@ export class RefreshTokenResponse extends Schema.Class( }) {} // ============================================================================= -// Errors +// Legacy Errors (kept for backwards compatibility) // ============================================================================= -export class AuthTokenExpiredError extends Schema.TaggedError( - "Amp/Auth/AuthTokenExpiredError" -)("AuthTokenExpiredError", {}) {} - -export class AuthRateLimitError extends Schema.TaggedError( - "Amp/Auth/AuthRateLimitError" -)("AuthRateLimitError", { - retryAfter: Schema.Int, - message: Schema.String -}) {} - -export class AuthRefreshError extends Schema.TaggedError( - "Amp/Auth/AuthRefreshError" -)("AuthRefreshError", { - status: Schema.Int, - message: Schema.String -}) {} - -export class AuthUserMismatchError extends Schema.TaggedError( - "Amp/Auth/AuthUserMismatchError" -)("AuthUserMismatchError", { - expected: Schema.String, - received: Schema.String -}) {} - export class VerifySignedAccessTokenError extends Schema.TaggedError( "Amp/Auth/VerifySignedAccessTokenError" )("VerifySignedAccessTokenError", { cause: Schema.Defect }) {} -export class DeviceTokenPendingError extends Schema.TaggedError( - "Amp/Auth/DeviceTokenPendingError" -)("DeviceTokenPendingError", {}) {} - -export class DeviceTokenExpiredError extends Schema.TaggedError( - "Amp/Auth/DeviceTokenExpiredError" -)("DeviceTokenExpiredError", {}) {} - // ============================================================================= // Service // ============================================================================= @@ -233,41 +227,52 @@ export class Auth extends Context.Tag("Amp/Auth") Effect.Effect< DeviceAuthorizationResponse, - HttpClientError.HttpClientError | TimeoutException | ParseResult.ParseError + AuthNetworkError | AuthRefreshError > readonly pollDeviceToken: (deviceCode: DeviceCode, codeVerifier: CodeVerifier) => Effect.Effect< AuthInfo, - | HttpClientError.HttpClientError - | ParseResult.ParseError - | PlatformError.PlatformError - | TimeoutException - | DeviceTokenPendingError - | DeviceTokenExpiredError + AuthNetworkError | AuthCacheError | AuthDeviceFlowError > readonly refreshAccessToken: (authInfo: AuthInfo) => Effect.Effect< AuthInfo, - | HttpClientError.HttpClientError - | ParseResult.ParseError - | PlatformError.PlatformError - | TimeoutException + | AuthNetworkError + | AuthCacheError | AuthTokenExpiredError | AuthRateLimitError | AuthRefreshError | AuthUserMismatchError > - readonly getCachedAuthInfo: Effect.Effect> - - readonly setCachedAuthInfo: (authInfo: AuthInfo) => Effect.Effect< - void, - ParseResult.ParseError | PlatformError.PlatformError + readonly generateAccessToken: (options: { + readonly authInfo: AuthInfo + readonly audience?: ReadonlyArray | undefined + readonly duration?: TokenDuration | undefined + }) => Effect.Effect< + GenerateTokenResponse, + | AuthNetworkError + | AuthTokenExpiredError + | AuthRateLimitError + | AuthRefreshError > - readonly clearCachedAuthInfo: Effect.Effect + readonly verifyAccessToken: ( + token: Redacted.Redacted, + issuer: string + ) => Effect.Effect + + readonly getCachedAuthInfo: Effect.Effect, AuthCacheError> + + readonly setCachedAuthInfo: (authInfo: AuthInfo) => Effect.Effect + + readonly clearCachedAuthInfo: Effect.Effect }>() {} +// ============================================================================= +// Service Implementation +// ============================================================================= + const make = Effect.gen(function*() { const store = yield* KeyValueStore.KeyValueStore const kvs = store.forSchema(AuthInfo) @@ -276,6 +281,98 @@ const make = Effect.gen(function*() { HttpClient.mapRequest(HttpClientRequest.prependUrl(AUTH_PLATFORM_BASE_URL.toString())) ) + // ------------------------------------------------------------------------ + // Error Handling Helpers + // ------------------------------------------------------------------------ + + /** + * Executes an authenticated HTTP request with standard error handling. + * Handles status codes (401, 403, 429) and wraps HTTP errors into SDK errors. + */ + const executeAuthenticatedRequest = ( + request: HttpClientRequest.HttpClientRequest, + endpoint: string, + decodeBody: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect< + A, + ParseResult.ParseError | HttpClientError.ResponseError + > + ) => + httpClient.execute(request).pipe( + Effect.timeout("15 seconds"), + Effect.flatMap( + HttpClientResponse.matchStatus({ + "2xx": decodeBody, + 401: () => + Effect.fail( + new AuthTokenExpiredError({ + message: "Access token is no longer valid (401 Unauthorized)" + }) + ), + 403: () => + Effect.fail( + new AuthTokenExpiredError({ + message: "Access token lacks required permissions (403 Forbidden)" + }) + ), + 429: Effect.fnUntraced(function*(response) { + const message = yield* extractErrorDescription(response) + const retryAfter = Option.fromNullable(response.headers["retry-after"]).pipe( + Option.flatMap((retryAfter) => { + const parsed = Number.parseInt(retryAfter, 10) + return Number.isNaN(parsed) + ? Option.none() + : Option.some(Duration.seconds(parsed)) + }), + Option.getOrElse(() => Duration.minutes(1)) + ) + return yield* new AuthRateLimitError({ message, retryAfter }) + }), + orElse: Effect.fnUntraced(function*(response) { + const message = yield* extractErrorDescription(response) + return yield* new AuthRefreshError({ + message, + status: Option.some(response.status), + cause: Option.none() + }) + }) + }) + ), + Effect.catchTag("TimeoutException", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Request to ${endpoint} timed out`, + endpoint: Option.some(endpoint), + isTimeout: true, + cause: Option.some(cause) + }) + )), + Effect.catchTag("RequestError", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Connection failed to ${endpoint}: ${cause.message}`, + endpoint: Option.some(endpoint), + isTimeout: false, + cause: Option.some(cause) + }) + )), + Effect.catchTag("ParseError", (cause) => + Effect.fail( + new AuthRefreshError({ + message: `Failed to parse ${endpoint} response: ${cause.message}`, + status: Option.none(), + cause: Option.some(cause) + }) + )), + Effect.catchTag("ResponseError", (cause) => + Effect.fail( + new AuthRefreshError({ + message: `Failed to read ${endpoint} response: ${cause.message}`, + status: Option.some(cause.response.status), + cause: Option.some(cause) + }) + )) + ) + // ------------------------------------------------------------------------ // OAuth2 Authorization Code Flow with PKCE // ------------------------------------------------------------------------ @@ -290,7 +387,8 @@ const make = Effect.gen(function*() { const requestDeviceAuthorization = Effect.fn("Auth.requestDeviceAuthorization")( function*(codeChallenge: string) { - return yield* httpClient.post("/api/v1/device/authorize", { + const endpoint = "/api/v1/device/authorize" + return yield* httpClient.post(endpoint, { acceptJson: true, body: HttpBody.unsafeJson({ code_challenge: codeChallenge, @@ -298,14 +396,49 @@ const make = Effect.gen(function*() { }) }).pipe( Effect.timeout("30 seconds"), - Effect.flatMap(HttpClientResponse.schemaBodyJson(DeviceAuthorizationResponse)) + Effect.flatMap(HttpClientResponse.schemaBodyJson(DeviceAuthorizationResponse)), + Effect.catchTag("TimeoutException", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Request to ${endpoint} timed out`, + endpoint: Option.some(endpoint), + isTimeout: true, + cause: Option.some(cause) + }) + )), + Effect.catchTag("RequestError", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Connection failed to ${endpoint}: ${cause.message}`, + endpoint: Option.some(endpoint), + isTimeout: false, + cause: Option.some(cause) + }) + )), + Effect.catchTag("ParseError", (cause) => + Effect.fail( + new AuthRefreshError({ + message: `Failed to parse ${endpoint} response: ${cause.message}`, + status: Option.none(), + cause: Option.some(cause) + }) + )), + Effect.catchTag("ResponseError", (cause) => + Effect.fail( + new AuthRefreshError({ + message: `Failed to read ${endpoint} response: ${cause.message}`, + status: Option.some(cause.response.status), + cause: Option.some(cause) + }) + )) ) } ) const pollDeviceToken = Effect.fn("Auth.pollDeviceToken")( function*(deviceCode: DeviceCode, codeVerifier: CodeVerifier) { - const response = yield* httpClient.get("/api/v1/device/token", { + const endpoint = "/api/v1/device/token" + const response = yield* httpClient.get(endpoint, { acceptJson: true, urlParams: UrlParams.fromInput({ "device_code": deviceCode, @@ -313,15 +446,58 @@ const make = Effect.gen(function*() { }) }).pipe( Effect.timeout("10 seconds"), - Effect.flatMap(HttpClientResponse.schemaBodyJson(DeviceTokenPollingResponse)) + Effect.flatMap(HttpClientResponse.schemaBodyJson(DeviceTokenPollingResponse)), + Effect.catchTag("TimeoutException", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Request to ${endpoint} timed out`, + endpoint: Option.some(endpoint), + isTimeout: true, + cause: Option.some(cause) + }) + )), + Effect.catchTag("RequestError", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Connection failed to ${endpoint}: ${cause.message}`, + endpoint: Option.some(endpoint), + isTimeout: false, + cause: Option.some(cause) + }) + )), + Effect.catchTag("ResponseError", (cause) => + Effect.fail( + new AuthNetworkError({ + message: `Device token request failed: ${cause.message}`, + endpoint: Option.some(endpoint), + isTimeout: false, + cause: Option.some(cause) + }) + )), + Effect.catchTag("ParseError", () => + Effect.fail( + new AuthDeviceFlowError({ + message: "Failed to parse device token response", + reason: "expired", + verificationUri: Option.none() + }) + )) ) if (response._tag === "DeviceTokenPendingResponse") { - return yield* new DeviceTokenPendingError() + return yield* new AuthDeviceFlowError({ + message: "Device authorization is still pending", + reason: "pending", + verificationUri: Option.none() + }) } if (response._tag === "DeviceTokenExpiredResponse") { - return yield* new DeviceTokenExpiredError() + return yield* new AuthDeviceFlowError({ + message: "Device authorization code has expired", + reason: "expired", + verificationUri: Option.none() + }) } const authInfo = yield* makeAuthInfo({ @@ -339,48 +515,49 @@ const make = Effect.gen(function*() { ) // ------------------------------------------------------------------------ - // OAuth2 Refresh Token + // OAuth2 Generate / Refresh Token // ------------------------------------------------------------------------ + const generateAccessToken = Effect.fn("Auth.generateAccessToken")( + function*({ authInfo, audience, duration }: { + readonly authInfo: AuthInfo + readonly audience?: ReadonlyArray | undefined + readonly duration?: TokenDuration | undefined + }) { + const endpoint = "/api/v1/auth/generate" + const request = HttpClientRequest.post(endpoint, { + body: HttpBody.unsafeJson(new GenerateTokenRequest({ audience, duration })), + acceptJson: true + }).pipe(HttpClientRequest.bearerToken(authInfo.accessToken)) + + return yield* executeAuthenticatedRequest( + request, + endpoint, + HttpClientResponse.schemaBodyJson(GenerateTokenResponse) + ) + } + ) + const refreshAccessToken = Effect.fn("Auth.refreshAccessToken")( function*(authInfo: AuthInfo) { - const request = HttpClientRequest.post("/refresh", { + const endpoint = "/api/v1/auth/refresh" + const request = HttpClientRequest.post(endpoint, { body: HttpBody.unsafeJson(RefreshTokenRequest.fromAuthInfo(authInfo)), acceptJson: true }).pipe(HttpClientRequest.bearerToken(authInfo.accessToken)) - const response = yield* httpClient.execute(request).pipe( - Effect.timeout("15 seconds"), - Effect.flatMap(HttpClientResponse.matchStatus({ - "2xx": (response) => HttpClientResponse.schemaBodyJson(RefreshTokenResponse)(response), - // Unauthorized - 401: () => Effect.fail(new AuthTokenExpiredError()), - // Insufficient Permissions - 403: () => Effect.fail(new AuthTokenExpiredError()), - // Too Many Requests - 429: Effect.fnUntraced(function*(response) { - const message = yield* extractErrorDescription(response) - const retryAfter = Option.fromNullable(response.headers["retry-after"]).pipe( - Option.flatMap((retryAfter) => { - const parsed = Number.parseInt(retryAfter, 10) - return Number.isNaN(parsed) ? Option.none() : Option.some(parsed) - }), - Option.getOrElse(() => 60) - ) - return yield* new AuthRateLimitError({ message, retryAfter }) - }), - orElse: Effect.fnUntraced(function*(response) { - const message = yield* extractErrorDescription(response) - return yield* new AuthRefreshError({ message, status: response.status }) - }) - })) + const response = yield* executeAuthenticatedRequest( + request, + endpoint, + HttpClientResponse.schemaBodyJson(RefreshTokenResponse) ) // Validate that the received user ID matches the cached user ID if (response.user.id !== authInfo.userId) { return yield* new AuthUserMismatchError({ - expected: authInfo.userId, - received: response.user.id + message: `Expected user ID ${authInfo.userId} but received ${response.user.id}`, + expectedUserId: authInfo.userId, + receivedUserId: response.user.id }) } @@ -399,13 +576,113 @@ const make = Effect.gen(function*() { } ) + // ------------------------------------------------------------------------ + // OAuth2 Token Verification + // ------------------------------------------------------------------------ + + const JWKS = Jose.createRemoteJWKSet(new URL("./.well-known/jwks.json", AUTH_PLATFORM_BASE_URL)) + + const verifyAccessToken = Effect.fn("Auth.verifyAccessToken")( + function*(token: Redacted.Redacted, issuer: string) { + const result = yield* Effect.tryPromise({ + try: () => Jose.jwtVerify(Redacted.value(token), JWKS, { issuer }), + catch: (cause) => { + if (!(cause instanceof Jose.errors.JOSEError)) { + return new AuthVerifyTokenError({ + message: `Unknown verification error: ${String(cause)}`, + reason: "unknown", + claim: Option.none(), + cause: Option.some(cause) + }) + } + switch (cause.code) { + case "ERR_JWT_EXPIRED": + return new AuthVerifyTokenError({ + message: `Token expired: ${cause.message}`, + reason: "expired", + claim: Option.fromNullable((cause as Jose.errors.JWTExpired).claim), + cause: Option.some(cause) + }) + case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED": + return new AuthVerifyTokenError({ + message: `Signature verification failed: ${cause.message}`, + reason: "invalid_signature", + claim: Option.none(), + cause: Option.some(cause) + }) + case "ERR_JWT_CLAIM_VALIDATION_FAILED": + return new AuthVerifyTokenError({ + message: `Claim validation failed: ${(cause as Jose.errors.JWTClaimValidationFailed).claim} - ${ + (cause as Jose.errors.JWTClaimValidationFailed).reason + }`, + reason: "invalid_claims", + claim: Option.fromNullable((cause as Jose.errors.JWTClaimValidationFailed).claim), + cause: Option.some(cause) + }) + case "ERR_JWKS_NO_MATCHING_KEY": + case "ERR_JWKS_TIMEOUT": + return new AuthVerifyTokenError({ + message: `JWKS error: ${cause.message}`, + reason: "jwks_error", + claim: Option.none(), + cause: Option.some(cause) + }) + default: + return new AuthVerifyTokenError({ + message: `Verification error: ${cause.message}`, + reason: "unknown", + claim: Option.none(), + cause: Option.some(cause) + }) + } + } + }) + return result.payload + } + ) + // ------------------------------------------------------------------------ // Cache Operations // ------------------------------------------------------------------------ const getCachedAuthInfo = Effect.gen(function*() { - const cache = yield* Effect.flatten(kvs.get(AUTH_INFO_CACHE_KEY)) + const cacheResult = yield* kvs.get(AUTH_INFO_CACHE_KEY).pipe( + // Treat "not found" as Option.none() before wrapping other errors + Effect.catchIf( + (error) => error._tag === "SystemError" && error.reason === "NotFound", + () => Effect.succeed(Option.none()) + ), + Effect.catchTag("SystemError", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache read failed: ${cause.message}`, + operation: "read", + cause: Option.some(cause) + }) + )), + Effect.catchTag("ParseError", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache read failed: ${cause.message}`, + operation: "read", + cause: Option.some(cause) + }) + )), + Effect.catchTag("BadArgument", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache read failed: ${cause.message}`, + operation: "read", + cause: Option.some(cause) + }) + )) + ) + + if (Option.isNone(cacheResult)) { + return Option.none() + } + const cache = cacheResult.value const now = yield* Clock.currentTimeMillis // Check if we need to refresh the token @@ -421,19 +698,46 @@ const make = Effect.gen(function*() { // If a refresh is required, perform the refresh request if (needsRefresh) { - return yield* refreshAccessToken(cache) + const refreshed = yield* refreshAccessToken(cache).pipe( + Effect.option // Catch refresh errors and return None + ) + return refreshed } // Token is still valid, return as is - return cache + return Option.some(cache) }).pipe( - Effect.option, Effect.withSpan("AuthService.getCachedAuthInfo") ) const setCachedAuthInfo = Effect.fn("Auth.setCachedAuthInfo")( function*(authInfo: AuthInfo) { - yield* kvs.set(AUTH_INFO_CACHE_KEY, authInfo) + yield* kvs.set(AUTH_INFO_CACHE_KEY, authInfo).pipe( + Effect.catchTag("SystemError", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache write failed: ${cause.message}`, + operation: "write", + cause: Option.some(cause) + }) + )), + Effect.catchTag("ParseError", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache write failed: ${cause.message}`, + operation: "write", + cause: Option.some(cause) + }) + )), + Effect.catchTag("BadArgument", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache write failed: ${cause.message}`, + operation: "write", + cause: Option.some(cause) + }) + )) + ) } ) @@ -442,6 +746,22 @@ const make = Effect.gen(function*() { (error) => error._tag === "SystemError" && error.reason === "NotFound", () => Effect.void ), + Effect.catchTag("SystemError", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache clear failed: ${cause.message}`, + operation: "clear", + cause: Option.some(cause) + }) + )), + Effect.catchTag("BadArgument", (cause) => + Effect.fail( + new AuthCacheError({ + message: `Cache clear failed: ${cause.message}`, + operation: "clear", + cause: Option.some(cause) + }) + )), Effect.withSpan("Auth.clearCachedAuthInfo") ) @@ -449,7 +769,9 @@ const make = Effect.gen(function*() { createChallenge, requestDeviceAuthorization, pollDeviceToken, + generateAccessToken, refreshAccessToken, + verifyAccessToken, getCachedAuthInfo, setCachedAuthInfo, clearCachedAuthInfo diff --git a/packages/amp/src/config.ts b/packages/amp/src/config.ts index a439d8d..b963fa4 100644 --- a/packages/amp/src/config.ts +++ b/packages/amp/src/config.ts @@ -15,7 +15,7 @@ import * as Stream from "effect/Stream" import * as fs from "node:fs" import * as path from "node:path" import * as ManifestBuilder from "./manifest-builder/service.ts" -import * as Models from "./Models.ts" +import * as Models from "./models.ts" export class ModuleContext { public definitionPath: string diff --git a/packages/amp/src/index.ts b/packages/amp/src/index.ts index 478be2e..02f7213 100644 --- a/packages/amp/src/index.ts +++ b/packages/amp/src/index.ts @@ -1,12 +1,17 @@ /** * An implementation of the Arrow Flight protocol. */ -export * as ArrowFlight from "./ArrowFlight.ts" +export * as ArrowFlight from "./arrow-flight.ts" /** * Utilities for performing authentication / authorization related operations. */ -export * as Auth from "./Auth.ts" +export * as Auth from "./auth/service.ts" + +/** + * Authentication error domain model. + */ +export * as AuthErrors from "./auth/error.ts" /** * Operations for interacting with the Amp administration API. diff --git a/packages/amp/src/manifest-builder/service.ts b/packages/amp/src/manifest-builder/service.ts index 6fe9f1d..e68d8b6 100644 --- a/packages/amp/src/manifest-builder/service.ts +++ b/packages/amp/src/manifest-builder/service.ts @@ -5,7 +5,7 @@ import * as Layer from "effect/Layer" import * as Predicate from "effect/Predicate" import * as Schema from "effect/Schema" import * as AdminApi from "../admin/service.ts" -import * as Models from "../Models.ts" +import * as Models from "../models.ts" export const ManifestBuildResult = Schema.Struct({ metadata: Models.DatasetMetadata, diff --git a/packages/amp/src/Models.ts b/packages/amp/src/models.ts similarity index 97% rename from packages/amp/src/Models.ts rename to packages/amp/src/models.ts index a312ad5..42f4888 100644 --- a/packages/amp/src/Models.ts +++ b/packages/amp/src/models.ts @@ -29,6 +29,27 @@ export const RefreshToken = Schema.NonEmptyTrimmedString.pipe( ).annotations({ identifier: "RefreshToken" }) export type RefreshToken = typeof RefreshToken.Type +const TOKEN_DURATION_REGEX = + /^-?\d+\.?\d*\s*(sec|secs|second|seconds|s|minute|minutes|min|mins|m|hour|hours|hr|hrs|h|day|days|d|week|weeks|w|year|years|yr|yrs|y)(\s+ago|\s+from\s+now)?$/i + +/** + * A branded type representing the duration an OAuth2 access token should be + * valid for. + */ +export const TokenDuration = Schema.NonEmptyTrimmedString.pipe( + Schema.pattern(TOKEN_DURATION_REGEX), + Schema.brand("TokenDuration") +).annotations({ + identifier: "TokenDuration", + examples: [ + "7 days" as TokenDuration, + "30 days" as TokenDuration, + "1 hour" as TokenDuration, + "1 year" as TokenDuration + ] +}) +export type TokenDuration = typeof TokenDuration.Type + /** * A branded type representing the identifier for an authenticated user. */ diff --git a/packages/amp/src/Protobuf/FlightSql_pb.ts b/packages/amp/src/protobuf/FlightSql_pb.ts similarity index 100% rename from packages/amp/src/Protobuf/FlightSql_pb.ts rename to packages/amp/src/protobuf/FlightSql_pb.ts diff --git a/packages/amp/src/Protobuf/Flight_pb.ts b/packages/amp/src/protobuf/Flight_pb.ts similarity index 100% rename from packages/amp/src/Protobuf/Flight_pb.ts rename to packages/amp/src/protobuf/Flight_pb.ts diff --git a/packages/amp/src/registry/api.ts b/packages/amp/src/registry/api.ts index 49dd516..5fb8861 100644 --- a/packages/amp/src/registry/api.ts +++ b/packages/amp/src/registry/api.ts @@ -4,7 +4,7 @@ import * as HttpApiError from "@effect/platform/HttpApiError" import * as HttpApiGroup from "@effect/platform/HttpApiGroup" import * as HttpApiSchema from "@effect/platform/HttpApiSchema" import * as Schema from "effect/Schema" -import * as Models from "../Models.ts" +import * as Models from "../models.ts" import * as Domain from "./domain.ts" import * as Errors from "./error.ts" diff --git a/packages/amp/src/registry/domain.ts b/packages/amp/src/registry/domain.ts index 3fecbcd..392b092 100644 --- a/packages/amp/src/registry/domain.ts +++ b/packages/amp/src/registry/domain.ts @@ -1,5 +1,5 @@ import * as Schema from "effect/Schema" -import * as Models from "../Models.ts" +import * as Models from "../models.ts" // TODO(Chris/Max/Sebastian): Should we consider moving these "general" schemas // to the top-level models module (Which I'm considering moving into a /domain diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 0000000..b7ad282 --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,70 @@ +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: Edge & Node Ventures, Inc. +Licensed Work: The Licensed Work is the specific version of the source code + with (c) 2025 Edge & Node Ventures, Inc. that this License is + included with. +Additional Use Grant: You may make production use of the Licensed Work, provided + Your use does not compete with Edge & Node Ventures’ offerings + of the Licensed Work. + + For purposes of this license: A “competitive offering” is a + Product that is offered to third parties on a paid basis, or a + Product that is offered to third parties and significantly + overlaps with the functionality and capabilities of Edge & Node + Ventures’ version(s) of the Licensed Work. Additionally, + Products that are not provided on a paid basis, or that do not + significantly overlap with the functionality and capabilities + of Edge & Node Ventures’ version(s) of the Licensed Work are + not competitive. + + “Product” means software that is offered to end users and + managed by the end users, or offered to end users through a + third party service that makes use of the Licensed Work. + +Change Date: Three years, or earlier, from the date the Licensed Work is published +Change License: Apache 2.0 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..b9f785a --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1 @@ +# Amp CLI diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..65b50e5 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,69 @@ +{ + "name": "@edgeandnode/amp-cli", + "version": "0.0.51", + "type": "module", + "license": "BUSL-1.1", + "description": "Build and manage blockchain datasets.", + "homepage": "https://www.edgeandnode.com/amp-dev", + "repository": { + "type": "git", + "url": "https://github.com/edgeandnode/amp-typescript", + "directory": "packages/cli" + }, + "bin": { + "amp": "./src/bin.ts" + }, + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + "./*": "./dist/*.ts", + "./bin": null, + "./main": null, + "./internal/*.ts": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "provenance": true, + "bin": { + "amp": "./src/bin.js" + }, + "exports": { + "./package.json": "./package.json", + "./*": "./dist/*.js", + "./bin": null, + "./main": null, + "./internal/*.ts": null + } + }, + "scripts": { + "build": "tsc -b tsconfig.json && pnpm babel", + "build:tsgo": "tsgo -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@edgeandnode/amp": "workspace:^", + "@effect/cli": "^0.73.1", + "@effect/platform": "^0.94.2", + "@effect/platform-node": "^0.104.1", + "effect": "^3.19.15" + }, + "devDependencies": { + "@connectrpc/connect": "^2.1.1", + "@effect/cli": "^0.73.1", + "@effect/platform": "^0.94.2", + "@effect/platform-node": "^0.104.1", + "effect": "^3.19.15" + }, + "dependencies": { + "open": "^11.0.0" + } +} diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts new file mode 100644 index 0000000..bfb9d21 --- /dev/null +++ b/packages/cli/src/bin.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import { Cli } from "./cli.ts" + +NodeRuntime.runMain(Cli, { + disableErrorReporting: true, + disablePrettyLogger: true +}) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000..db3b44a --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,58 @@ +import * as Auth from "@edgeandnode/amp/auth/service" +import * as CliConfig from "@effect/cli/CliConfig" +import * as Command from "@effect/cli/Command" +import * as NodeContext from "@effect/platform-node/NodeContext" +import * as FetchHttpClient from "@effect/platform/FetchHttpClient" +import * as KeyValueStore from "@effect/platform/KeyValueStore" +import * as Path from "@effect/platform/Path" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as NodeOS from "node:os" +import PackageJson from "../package.json" with { type: "json" } +import { AuthCommand } from "./commands/auth.ts" +import { QueryCommand } from "./commands/query.ts" + +const RootCommand = Command.make("amp").pipe( + Command.withSubcommands([AuthCommand, QueryCommand]) +) + +const run = Command.run(RootCommand, { + name: "Amp", + version: PackageJson["version"] +}) + +const CliConfigLayer = CliConfig.layer({ + showBuiltIns: false +}) + +const CliCacheLayer = Layer.unwrapEffect( + Effect.gen(function*() { + const path = yield* Path.Path + + const homeDirectory = NodeOS.homedir() + const ampCachePath = path.join(homeDirectory, ".amp", "cache") + + return KeyValueStore.layerFileSystem(ampCachePath) + }) +) + +const HttpClientLayer = FetchHttpClient.layer + +const AuthLayer = Auth.layer.pipe( + Layer.provide(CliCacheLayer), + Layer.provide(HttpClientLayer) +) + +const MainLayer = Layer.mergeAll( + AuthLayer, + CliConfigLayer, + HttpClientLayer +).pipe( + Layer.provideMerge(NodeContext.layer), + Layer.orDie +) + +export const Cli = run(process.argv).pipe( + Effect.provide(MainLayer), + Effect.catchTag("Amp/NonZeroExitCode", () => Effect.sync(() => process.exit(1))) +) diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts new file mode 100644 index 0000000..62e8e52 --- /dev/null +++ b/packages/cli/src/commands/auth.ts @@ -0,0 +1,9 @@ +import * as Command from "@effect/cli/Command" +import { LoginCommand } from "./auth/login.ts" +import { LogoutCommand } from "./auth/logout.ts" +import { TokenCommand } from "./auth/token.ts" + +export const AuthCommand = Command.make("auth").pipe( + Command.withDescription("Commands used to login to, logout of, and obtain tokens to interact with Amp."), + Command.withSubcommands([LoginCommand, LogoutCommand, TokenCommand]) +) diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts new file mode 100644 index 0000000..c232e05 --- /dev/null +++ b/packages/cli/src/commands/auth/login.ts @@ -0,0 +1,121 @@ +import * as Auth from "@edgeandnode/amp/auth/service" +import * as Command from "@effect/cli/Command" +import * as Prompt from "@effect/cli/Prompt" +import * as Console from "effect/Console" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Option from "effect/Option" +import * as Schedule from "effect/Schedule" +import * as String from "effect/String" +import Open from "open" + +const handleLoginCommand = Effect.fnUntraced(function*() { + const auth = yield* Auth.Auth + + const authInfo = yield* auth.getCachedAuthInfo + + // User already authenticated + if (Option.isSome(authInfo)) { + yield* Console.error("You are already authenticated with Amp.") + return yield* Effect.void + } + + // Perform OAuth2 PKCE flow + const { codeChallenge, codeVerifier } = yield* auth.createChallenge + + const { + expiresIn, + deviceCode, + interval, + userCode, + verificationUri + } = yield* auth.requestDeviceAuthorization(codeChallenge) + + // Show the user the OAuth2 PKCE code + yield* Console.error(String.stripMargin( + `|Copy the following verification code and enter it in your browser: + | + | ${userCode} + |` + )) + + // Ask if we should auto-open the user's browser + const autoOpenBrowser = yield* Prompt.confirm({ + message: "Would you like to open your browser automatically?", + initial: true + }).pipe(Effect.zipLeft(Console.error())) + + if (autoOpenBrowser) { + // If so, attempt to open the browser, falling back to a useful message + yield* Effect.try(() => Open(verificationUri, { wait: false })).pipe( + Effect.catchAllCause(() => + Console.error(String.stripMargin( + `|If the browser window does not open automatically, enter the verification code into the following URL: + | + | ${verificationUri} + |` + )) + ) + ) + } else { + // If not, indicate that the user show navigate to the verification URL + yield* Console.error(String.stripMargin( + `|Enter the verification code into the following URL: + | + | ${verificationUri} + |` + )) + } + + // Initially starts polling with a faster exponential backoff (1s, 1.5s, 2.25s, ...), + // but then caps at the server's requested interval, setting the maximum + // number of polling attempts based on the device code's lifetime + const pollingSchedule = Schedule.exponential("1 second", 1.5).pipe( + Schedule.union(Schedule.spaced(Duration.seconds(interval))), + Schedule.intersect(Schedule.recurs(Math.floor(expiresIn / interval))) + ) + + // Show a spinner while we wait + const spinnerFiber = yield* Effect.fork(showSpinner("Waiting for the user to authenticate...")) + + // Poll for the auth info response + const response = yield* auth.pollDeviceToken(deviceCode, codeVerifier).pipe( + Effect.retry({ + schedule: pollingSchedule, + while: (error) => error._tag === "AuthDeviceFlowError" && error.reason === "pending" + }), + Effect.tapErrorCause(() => Console.error("Authentication timed out or failed. Please try again.")), + Effect.ensuring(Fiber.interrupt(spinnerFiber)) + ) + + // Cache the auth information so it can be used by other commands + yield* auth.setCachedAuthInfo(response) + + yield* Console.error("Authenticated successfully!") +}) + +export const LoginCommand = Command.make("login").pipe( + Command.withDescription("Login to the Amp CLI"), + Command.withHandler(handleLoginCommand) +) + +// ============================================================================= +// Internal Utilities +// ============================================================================= + +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + +const showSpinner = Effect.fnUntraced(function*(message) { + let index = 0 + return yield* Effect.sync(() => { + const frame = SPINNER_FRAMES[index] + const spinner = `\r\x1b[36m${frame}\x1b[0m ${message}` + process.stdout.write(spinner) + index = (index + 1) % SPINNER_FRAMES.length + }).pipe( + Effect.schedule(Schedule.fixed("80 millis")), + // Make sure to cleanup the spinner output + Effect.ensuring(Effect.sync(() => process.stdout.write("\r\x1b[K"))) + ) +}) diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts new file mode 100644 index 0000000..9f8aaa5 --- /dev/null +++ b/packages/cli/src/commands/auth/logout.ts @@ -0,0 +1,27 @@ +import * as Auth from "@edgeandnode/amp/auth/service" +import * as Command from "@effect/cli/Command" +import * as Prompt from "@effect/cli/Prompt" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" + +const handleLogoutCommand = Effect.fnUntraced(function*() { + const auth = yield* Auth.Auth + + const shouldLogout = yield* Prompt.confirm({ + message: "Are you sure you want to logout of Amp?", + initial: false + }) + + if (!shouldLogout) { + return yield* Console.error("Logout cancelled, exiting...") + } + + yield* auth.clearCachedAuthInfo + + yield* Console.error("You have successfully logged out!") +}) + +export const LogoutCommand = Command.make("logout").pipe( + Command.withDescription("Logout of the Amp CLI"), + Command.withHandler(handleLogoutCommand) +) diff --git a/packages/cli/src/commands/auth/token.ts b/packages/cli/src/commands/auth/token.ts new file mode 100644 index 0000000..ff17f64 --- /dev/null +++ b/packages/cli/src/commands/auth/token.ts @@ -0,0 +1,94 @@ +import * as Auth from "@edgeandnode/amp/auth/service" +import * as Models from "@edgeandnode/amp/models" +import * as Args from "@effect/cli/Args" +import * as Command from "@effect/cli/Command" +import * as Options from "@effect/cli/Options" +import * as Console from "effect/Console" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Redacted from "effect/Redacted" +import * as String from "effect/String" +import * as Errors from "../../errors.ts" + +const audience = Options.text("audience").pipe( + Options.withAlias("a"), + Options.withDescription( + "URLs that are valid to use the generated access token. " + + "Becomes the JWT aud value" + ), + Options.repeated +) + +const duration = Args.text({ name: "duration" }).pipe( + Args.withDescription( + "Duration of the generated access token before it expires. " + + "Ex: \"7 days\", \"30 days\", \"1 hour\"" + ), + Args.withSchema(Models.TokenDuration) +) + +const handleTokenCommand = Effect.fnUntraced(function*({ audience, duration }: { + readonly audience: Array + readonly duration: Models.TokenDuration +}) { + const auth = yield* Auth.Auth + + const authInfo = yield* auth.getCachedAuthInfo.pipe( + Effect.flatten, + Effect.catchTag( + "NoSuchElementException", + Effect.fnUntraced(function*() { + const errorMessage = [ + "You must be authenticated with Amp to generate an access token.", + "Run \"amp auth login\" to authenticate." + ].join(" ") + yield* Console.error(errorMessage) + return yield* new Errors.NonZeroExitCode() + }) + ) + ) + + const response = yield* auth.generateAccessToken({ authInfo, audience, duration }).pipe( + Effect.catchAll(Effect.fnUntraced(function*(error) { + const errorMessage = `${error.userMessage}. ${error.userSuggestion}` + yield* Console.error(errorMessage) + return yield* new Errors.NonZeroExitCode() + })) + ) + + yield* auth.verifyAccessToken(Redacted.make(response.token), response.iss).pipe( + Effect.catchAll(Effect.fnUntraced(function*(error) { + const errorMessage = [ + "Failed to verify the signed token.", + error.userMessage, + error.userSuggestion + ].join("\n") + yield* Console.error(errorMessage) + return yield* new Errors.NonZeroExitCode() + })) + ) + + const expiresAt = DateTime.unsafeMake(response.exp * 1000) + const formatDateTime = DateTime.formatLocal({ + timeStyle: "full", + dateStyle: "medium" + }) + const message = [ + "Access token generated successfully!", + "We do not store this value - make sure you store it securely.", + "You can use this token as an bearer authorization header in requests to Amp", + String.stripMargin( + `| token: ${response.token} + | expires: ${formatDateTime(expiresAt)}` + ) + ].join("\n\n") + + yield* Console.error(message) +}) + +export const TokenCommand = Command.make("token", { audience, duration }).pipe( + Command.withDescription( + "Generates an access token (Bearer JWT) to be used by your applictaion to interact with Amp" + ), + Command.withHandler(handleTokenCommand) +) diff --git a/packages/cli/src/commands/query.ts b/packages/cli/src/commands/query.ts new file mode 100644 index 0000000..b14fa25 --- /dev/null +++ b/packages/cli/src/commands/query.ts @@ -0,0 +1,75 @@ +import * as ArrowFlight from "@edgeandnode/amp/arrow-flight" +import * as Args from "@effect/cli/Args" +import * as Command from "@effect/cli/Command" +import * as Options from "@effect/cli/Options" +import * as Console from "effect/Console" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import type * as Redacted from "effect/Redacted" +import * as ArrowFlightAuth from "../services/arrow-flight.ts" + +type ResultFormat = "json" | "jsonl" | "pretty" | "table" +const ResultFormats: ReadonlyArray = ["json", "jsonl", "pretty", "table"] + +// TODO(Chris): we should re-evaluate this format option +const format = Options.choice("format", ResultFormats).pipe( + Options.withAlias("f"), + Options.withDescription("The format to output the results in."), + Options.withDefault("table") +) + +const limit = Options.integer("limit").pipe( + Options.withDescription("The number of rows to return from the query."), + Options.optional +) + +const query = Args.text({ name: "query" }).pipe( + Args.withDescription("The SQL query to execute.") +) + +const token = Options.redacted("token").pipe( + Options.withAlias("t"), + Options.withDescription("The bearer token to use for authentication."), + Options.optional +) + +const queryCommandHandler = Effect.fnUntraced(function*(params: { + readonly format: ResultFormat + readonly limit: Option.Option + readonly query: string + readonly token: Option.Option> +}) { + const flight = yield* ArrowFlight.ArrowFlight + + const query = Option.match(params.limit, { + onNone: () => params.query, + onSome: (limit) => `${params.query} LIMIT ${limit}` + }) + + const results = yield* flight.query(query) + + const data = results + .filter(({ data }) => data.length > 0) + .flatMap(({ data }) => data) + + switch (params.format) { + case "json": { + return yield* Console.log(JSON.stringify(data, null, 2)) + } + case "jsonl": { + return yield* Console.log(JSON.stringify(data)) + } + case "pretty": { + return yield* Console.log(data) + } + case "table": { + return yield* Console.table(data) + } + } +}) + +export const QueryCommand = Command.make("query", { format, limit, query, token }).pipe( + Command.withDescription("Execute a SQL query with Amp"), + Command.withHandler(queryCommandHandler), + Command.provide(({ token }) => ArrowFlightAuth.layerToken(token)) +) diff --git a/packages/cli/src/errors.ts b/packages/cli/src/errors.ts new file mode 100644 index 0000000..f2bed6a --- /dev/null +++ b/packages/cli/src/errors.ts @@ -0,0 +1,3 @@ +import * as Data from "effect/Data" + +export class NonZeroExitCode extends Data.TaggedError("Amp/NonZeroExitCode") {} diff --git a/packages/cli/src/services/arrow-flight.ts b/packages/cli/src/services/arrow-flight.ts new file mode 100644 index 0000000..e649715 --- /dev/null +++ b/packages/cli/src/services/arrow-flight.ts @@ -0,0 +1,46 @@ +import type { Interceptor } from "@connectrpc/connect" +import * as ArrowFlight from "@edgeandnode/amp/arrow-flight" +import * as NodeArrowFlight from "@edgeandnode/amp/arrow-flight/node" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Redacted from "effect/Redacted" + +/** + * A layer which constructs an `ArrowFlight` service which will used cached + * credentials for authentication. + */ +export const layer = ArrowFlight.layer.pipe( + Layer.provide(NodeArrowFlight.layerTransportGrpc({ + baseUrl: "http://localhost:1602" + })), + Layer.provide(ArrowFlight.layerInterceptorBearerAuth) +) + +/** + * A layer which constructs an `ArrowFlight` service which will used cached + * credentials for authentication. + * + * Also accepts a generated access token which can be used for authentication. + */ +export const layerToken = (token: Option.Option>) => + ArrowFlight.layer.pipe( + Layer.provide(NodeArrowFlight.layerTransportGrpc({ + baseUrl: "http://localhost:1602" + })), + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const interceptors = yield* ArrowFlight.Interceptors + if (Option.isSome(token)) { + const interceptor: Interceptor = (next) => (request) => { + const accessToken = Redacted.value(token.value) + request.header.append("Authorization", `Bearer ${accessToken}`) + return next(request) + } + const context = Context.make(ArrowFlight.Interceptors, Arr.append(interceptors, interceptor)) + return Layer.succeedContext(context) + } + return ArrowFlight.layerInterceptorBearerAuth + }))) + ) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..911bee9 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [], + "compilerOptions": { + "erasableSyntaxOnly": false, + "resolveJsonModule": true, + "types": ["node"] + } +} diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts new file mode 100644 index 0000000..fb966ae --- /dev/null +++ b/packages/cli/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/packages/tools/oxc/package.json b/packages/tools/oxc/package.json index a012305..f2ae3eb 100644 --- a/packages/tools/oxc/package.json +++ b/packages/tools/oxc/package.json @@ -59,7 +59,7 @@ "coverage": "vitest --coverage" }, "devDependencies": { - "@types/node": "^25.0.10", + "@types/node": "^25.1.0", "vitest": "^4.0.18" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d14849c..65ae904 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,11 +39,11 @@ importers: specifier: ^0.27.0 version: 0.27.0(effect@3.19.15)(vitest@4.0.18) '@types/node': - specifier: ^25.0.10 - version: 25.0.10 + specifier: ^25.1.0 + version: 25.1.0 '@typescript/native-preview': - specifier: 7.0.0-dev.20260122.4 - version: 7.0.0-dev.20260122.4 + specifier: 7.0.0-dev.20260130.1 + version: 7.0.0-dev.20260130.1 '@vitest/coverage-v8': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18) @@ -63,14 +63,14 @@ importers: specifier: ^13.0.0 version: 13.0.0 globals: - specifier: ^17.1.0 - version: 17.1.0 + specifier: ^17.2.0 + version: 17.2.0 madge: specifier: ^8.0.0 version: 8.0.0(typescript@5.9.3) oxlint: - specifier: ^1.41.0 - version: 1.41.0 + specifier: ^1.42.0 + version: 1.42.0 ts-patch: specifier: ^3.3.0 version: 3.3.0 @@ -78,11 +78,11 @@ importers: specifier: ^5.9.3 version: 5.9.3 vite-tsconfig-paths: - specifier: ^6.0.4 - version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2)) + specifier: ^6.0.5 + version: 6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2)) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.0.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + version: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) vitest-mock-express: specifier: ^2.2.0 version: 2.2.0 @@ -92,9 +92,12 @@ importers: jiti: specifier: ^2.6.1 version: 2.6.1 + jose: + specifier: ^6.1.3 + version: 6.1.3 viem: - specifier: ^2.44.4 - version: 2.44.4(typescript@5.9.3) + specifier: ^2.45.1 + version: 2.45.1(typescript@5.9.3) devDependencies: '@bufbuild/buf': specifier: ^1.64.0 @@ -118,14 +121,39 @@ importers: specifier: ^3.19.15 version: 3.19.15 + packages/cli: + dependencies: + '@edgeandnode/amp': + specifier: workspace:^ + version: link:../amp + open: + specifier: ^11.0.0 + version: 11.0.0 + devDependencies: + '@connectrpc/connect': + specifier: ^2.1.1 + version: 2.1.1(@bufbuild/protobuf@2.11.0) + '@effect/cli': + specifier: ^0.73.1 + version: 0.73.1(@effect/platform@0.94.2(effect@3.19.15))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15))(effect@3.19.15) + '@effect/platform': + specifier: ^0.94.2 + version: 0.94.2(effect@3.19.15) + '@effect/platform-node': + specifier: ^0.104.1 + version: 0.104.1(@effect/cluster@0.56.0(@effect/platform@0.94.2(effect@3.19.15))(@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(effect@3.19.15) + effect: + specifier: ^3.19.15 + version: 3.19.15 + packages/tools/oxc: devDependencies: '@types/node': - specifier: ^25.0.10 - version: 25.0.10 + specifier: ^25.1.0 + version: 25.1.0 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.0.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + version: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) scratchpad: dependencies: @@ -366,6 +394,14 @@ packages: cpu: [x64] os: [win32] + '@effect/cli@0.73.1': + resolution: {integrity: sha512-lKDNq1Dwb+f1aPbse+EPORbNhQSJvefkEeFiIdWQJB4YX/D/9cibGGA1rb4A1PHn8qJhrE376DG3d0TlszxR+w==} + peerDependencies: + '@effect/platform': ^0.94.2 + '@effect/printer': ^0.47.0 + '@effect/printer-ansi': ^0.47.0 + effect: ^3.19.15 + '@effect/cluster@0.56.0': resolution: {integrity: sha512-ovhsC8jQkgoHkelpGL/EQtoQsPXG9hj7DHVc7A9Vyzptd/DaCP+W1aQKlJrJoe2W+KI/CjrYN8H89M5xiL6FBg==} peerDependencies: @@ -415,6 +451,18 @@ packages: peerDependencies: effect: ^3.19.15 + '@effect/printer-ansi@0.47.0': + resolution: {integrity: sha512-tDEQ9XJpXDNYoWMQJHFRMxKGmEOu6z32x3Kb8YLOV5nkauEKnKmWNs7NBp8iio/pqoJbaSwqDwUg9jXVquxfWQ==} + peerDependencies: + '@effect/typeclass': ^0.38.0 + effect: ^3.19.0 + + '@effect/printer@0.47.0': + resolution: {integrity: sha512-VgR8e+YWWhMEAh9qFOjwiZ3OXluAbcVLIOtvp2S5di1nSrPOZxj78g8LE77JSvyfp5y5bS2gmFW+G7xD5uU+2Q==} + peerDependencies: + '@effect/typeclass': ^0.38.0 + effect: ^3.19.0 + '@effect/rpc@0.73.0': resolution: {integrity: sha512-iMPf6tTriz8sK0l5x4koFId8Hz5nFptHYg8WqyjHGIIVLTpZxuiSqhmXZG7FnAs5N2n6uCEws4wWGcIgXNUrFg==} peerDependencies: @@ -428,6 +476,11 @@ packages: '@effect/platform': ^0.94.0 effect: ^3.19.13 + '@effect/typeclass@0.38.0': + resolution: {integrity: sha512-lMUcJTRtG8KXhXoczapZDxbLK5os7M6rn0zkvOgncJW++A0UyelZfMVMKdT5R+fgpZcsAU/1diaqw3uqLJwGxA==} + peerDependencies: + effect: ^3.19.0 + '@effect/vitest@0.27.0': resolution: {integrity: sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ==} peerDependencies: @@ -667,43 +720,43 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@oxlint/darwin-arm64@1.41.0': - resolution: {integrity: sha512-K0Bs0cNW11oWdSrKmrollKF44HMM2HKr4QidZQHMlhJcSX8pozxv0V5FLdqB4sddzCY0J9Wuuw+oRAfR8sdRwA==} + '@oxlint/darwin-arm64@1.42.0': + resolution: {integrity: sha512-ui5CdAcDsXPQwZQEXOOSWsilJWhgj9jqHCvYBm2tDE8zfwZZuF9q58+hGKH1x5y0SV4sRlyobB2Quq6uU6EgeA==} cpu: [arm64] os: [darwin] - '@oxlint/darwin-x64@1.41.0': - resolution: {integrity: sha512-1LCCXCe9nN8LbrJ1QOGari2HqnxrZrveYKysWDIg8gFsQglIg00XF/8lRbA0kWHMdLgt4X0wfNYhhFz+c3XXLQ==} + '@oxlint/darwin-x64@1.42.0': + resolution: {integrity: sha512-wo0M/hcpHRv7vFje99zHHqheOhVEwUOKjOgBKyi0M99xcLizv04kcSm1rTd6HSCeZgOtiJYZRVAlKhQOQw2byQ==} cpu: [x64] os: [darwin] - '@oxlint/linux-arm64-gnu@1.41.0': - resolution: {integrity: sha512-Fow7H84Bs8XxuaK1yfSEWBC8HI7rfEQB9eR2A0J61un1WgCas7jNrt1HbT6+p6KmUH2bhR+r/RDu/6JFAvvj4g==} + '@oxlint/linux-arm64-gnu@1.42.0': + resolution: {integrity: sha512-j4QzfCM8ks+OyM+KKYWDiBEQsm5RCW50H1Wz16wUyoFsobJ+X5qqcJxq6HvkE07m8euYmZelyB0WqsiDoz1v8g==} cpu: [arm64] os: [linux] - '@oxlint/linux-arm64-musl@1.41.0': - resolution: {integrity: sha512-WoRRDNwgP5W3rjRh42Zdx8ferYnqpKoYCv2QQLenmdrLjRGYwAd52uywfkcS45mKEWHeY1RPwPkYCSROXiGb2w==} + '@oxlint/linux-arm64-musl@1.42.0': + resolution: {integrity: sha512-g5b1Uw7zo6yw4Ymzyd1etKzAY7xAaGA3scwB8tAp3QzuY7CYdfTwlhiLKSAKbd7T/JBgxOXAGNcLDorJyVTXcg==} cpu: [arm64] os: [linux] - '@oxlint/linux-x64-gnu@1.41.0': - resolution: {integrity: sha512-75k3CKj3fOc/a/2aSgO81s3HsTZOFROthPJ+UI2Oatic1LhvH6eKjKfx3jDDyVpzeDS2qekPlc/y3N33iZz5Og==} + '@oxlint/linux-x64-gnu@1.42.0': + resolution: {integrity: sha512-HnD99GD9qAbpV4q9iQil7mXZUJFpoBdDavfcC2CgGLPlawfcV5COzQPNwOgvPVkr7C0cBx6uNCq3S6r9IIiEIg==} cpu: [x64] os: [linux] - '@oxlint/linux-x64-musl@1.41.0': - resolution: {integrity: sha512-8r82eBwGPoAPn67ZvdxTlX/Z3gVb+ZtN6nbkyFzwwHWAh8yGutX+VBcVkyrePSl6XgBP4QAaddPnHmkvJjqY0g==} + '@oxlint/linux-x64-musl@1.42.0': + resolution: {integrity: sha512-8NTe8A78HHFn+nBi+8qMwIjgv9oIBh+9zqCPNLH56ah4vKOPvbePLI6NIv9qSkmzrBuu8SB+FJ2TH/G05UzbNA==} cpu: [x64] os: [linux] - '@oxlint/win32-arm64@1.41.0': - resolution: {integrity: sha512-aK+DAcckQsNCOXKruatyYuY/ROjNiRejQB1PeJtkZwM21+8rV9ODYbvKNvt0pW+YCws7svftBSFMCpl3ke2unw==} + '@oxlint/win32-arm64@1.42.0': + resolution: {integrity: sha512-lAPS2YAuu+qFqoTNPFcNsxXjwSV0M+dOgAzzVTAN7Yo2ifj+oLOx0GsntWoM78PvQWI7Q827ZxqtU2ImBmDapA==} cpu: [arm64] os: [win32] - '@oxlint/win32-x64@1.41.0': - resolution: {integrity: sha512-dVBXkZ6MGLd3owV7jvuqJsZwiF3qw7kEkDVsYVpS/O96eEvlHcxVbaPjJjrTBgikXqyC22vg3dxBU7MW0utGfw==} + '@oxlint/win32-x64@1.42.0': + resolution: {integrity: sha512-3/KmyUOHNriL6rLpaFfm9RJxdhpXY2/Ehx9UuorJr2pUA+lrZL15FAEx/DOszYm5r10hfzj40+efAHcCilNvSQ==} cpu: [x64] os: [win32] @@ -972,8 +1025,8 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node@25.0.10': - resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} + '@types/node@25.1.0': + resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1016,43 +1069,43 @@ packages: resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-8hIIPe6LoY+FmUBRvhX7IbsZCr4Puwps0Ok+ez+rzg7d+sJCVuJ37ZxFh1pcNJEM39eII2o5TZ9dTCIT7/aAWA==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-Jo5kVoxaewKPn/3bKWyUB/gPR+Tjhj6isLc8VshV4OyFX4n6pkvVyk3ANivl7Kwmiv3WGKGUotbZ71DKCZATwA==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-kJNpVzPW9jcFuRco+QwSHToEFHmfovNmwLo9PY97DLMqGnrvxkSy+k5sClHscEThIdzSRQbi7uZJUVBwgohNmA==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-dR0fjdcLykfiDOIKjZMGqPBHVl9Dd/C+jFU43Wr3dcPFPFf1oVYsaWAZBSkTXnN9QP8i0/ZV+ZUr1gDjoi3x0Q==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-QsSNK3PbRyNviv0xru21jGV8fgzTVJ26oMnQwoK0IcyGabXtcLHJe5I5y4lwvLNjZPCbKDNThawso54Fo72FJQ==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-P/1YTpIiFd2pPtHt4sKEmUTaKf1xvuuiV0TvhQ7n2gDYskNjZ66iWCC9w7okjgsmWE9JLh/IRrNcb9FKVk3SHw==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-ImSMhsrB9QMK/cT2s4yCWkWOBUGZ+1L6vBaMMdyw3eNrU4tQo85Z3AqTn12M8WRbefLL89OmP6zJppP2TH3PAQ==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-wnx4bY/1u006U67fEkPtPVZ65VYMLgkFqOadGyrUxhtveR5WbbgFUuUBES0mPxvzS4ToZzn94jhcnAvN8VOTcA==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-0OyzmbZ2Zq039RPLCyEjcrBhDkCXDfIrwbMJfoOrCyEO9XrK9Icwqzogm7H4bLyhcfuZIgugYSP2mQflaDvOVg==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-OgHVjivuOS22WIZvIm+Pnm7yqFLwonkIrBOxRdew/pPwVGLQVSo+bQ+RocQDj2VFYxXcHs2yXwCk3PDmwLIYYg==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-FSRZ0iylGKbrYiimfkqKBXRlhR+PwOz5Jcb57dAU7wzr/7xSGON9bfywb0BhoF31GjYAnZ5Xv+1fuQOiB2HBYw==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-f/DUxQtIWkZq0eUjZHFmaSxterO/ccu1NxFk0L/Oqj7AfjWVDCqrLVgZJKjvwcG5TEb5AVt7GMUpGEAYZQiUvg==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-vAzRnS4nMBAd2XduzdmrhsCAAVSbCzZxVGMIKVvRcL9ljO5+fooggiYq7sk798TIZ1ov7A0rZk5k+o0Wyx2nXA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-Isr051Cq8RbXOUMYYmwLYw8yBGaEG/Zp0sp7HNeYhVVkc3/3KeveEqCk29q1QRwiBr7HnApdzJP7f+lSZk8gmg==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260122.4': - resolution: {integrity: sha512-lboRukXxL3jeIyMKc4EjNHI4QThjOrrT5jjCyaNOUFkrk3JiObrR4z7Z6Z+m+WM/gbSW2tqaRBRFBRMsZGsxKQ==} + '@typescript/native-preview@7.0.0-dev.20260130.1': + resolution: {integrity: sha512-lvt9sECmBkrABxl3rMNRAX2unzhYcoNhlTyR7rOvbyM//QTXKUctVD7ByWBvk02et2caUUwIWq2vnygaeW8Mew==} hasBin: true '@typescript/vfs@1.6.2': @@ -1199,6 +1252,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + caniuse-lite@1.0.30001766: resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} @@ -1267,9 +1324,21 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + dependency-tree@11.2.0: resolution: {integrity: sha512-+C1H3mXhcvMCeu5i2Jpg9dc0N29TWTuT6vJD7mHLAfVmAbo9zW8NlkvQ1tYd3PDMab0IRQM0ccoyX68EZtx9xw==} engines: {node: '>=18'} @@ -1459,8 +1528,8 @@ packages: resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==} engines: {node: '>=16'} - globals@17.1.0: - resolution: {integrity: sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw==} + globals@17.2.0: + resolution: {integrity: sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==} engines: {node: '>=18'} globrex@0.1.2: @@ -1510,6 +1579,11 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1518,6 +1592,15 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -1545,6 +1628,10 @@ packages: is-url@1.2.4: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isexe@3.1.1: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} @@ -1570,6 +1657,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1715,6 +1805,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -1727,12 +1821,12 @@ packages: typescript: optional: true - oxlint@1.41.0: - resolution: {integrity: sha512-Dyaoup82uhgAgp5xLNt4dPdvl5eSJTIzqzL7DcKbkooUE4PDViWURIPlSUF8hu5a+sCnNIp/LlQMDsKoyaLTBA==} + oxlint@1.42.0: + resolution: {integrity: sha512-qnspC/lrp8FgKNaONLLn14dm+W5t0SSlus6V5NJpgI2YNT1tkFYZt4fBf14ESxf9AAh98WBASnW5f0gtw462Lg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.11.1' + oxlint-tsgolint: '>=0.11.2' peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -1784,6 +1878,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + precinct@12.2.0: resolution: {integrity: sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==} engines: {node: '>=18'} @@ -1838,6 +1936,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -1945,6 +2047,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -2002,21 +2107,18 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - viem@2.44.4: - resolution: {integrity: sha512-sJDLVl2EsS5Fo7GSWZME5CXEV7QRYkUJPeBw7ac+4XI3D4ydvMw/gjulTsT5pgqcpu70BploFnOAC6DLpan1Yg==} + viem@2.45.1: + resolution: {integrity: sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==} peerDependencies: typescript: ^5.9.3 peerDependenciesMeta: typescript: optional: true - vite-tsconfig-paths@6.0.4: - resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} + vite-tsconfig-paths@6.0.5: + resolution: {integrity: sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig==} peerDependencies: vite: '*' - peerDependenciesMeta: - vite: - optional: true vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} @@ -2139,6 +2241,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2378,6 +2484,16 @@ snapshots: '@dprint/win32-x64@0.51.1': optional: true + '@effect/cli@0.73.1(@effect/platform@0.94.2(effect@3.19.15))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15))(effect@3.19.15)': + dependencies: + '@effect/platform': 0.94.2(effect@3.19.15) + '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15) + '@effect/printer-ansi': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15) + effect: 3.19.15 + ini: 4.1.3 + toml: 3.0.0 + yaml: 2.8.2 + '@effect/cluster@0.56.0(@effect/platform@0.94.2(effect@3.19.15))(@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/sql@0.49.0(@effect/experimental@0.58.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(effect@3.19.15))(effect@3.19.15)': dependencies: '@effect/platform': 0.94.2(effect@3.19.15) @@ -2431,6 +2547,17 @@ snapshots: msgpackr: 1.11.8 multipasta: 0.2.7 + '@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15)': + dependencies: + '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15) + '@effect/typeclass': 0.38.0(effect@3.19.15) + effect: 3.19.15 + + '@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.15))(effect@3.19.15)': + dependencies: + '@effect/typeclass': 0.38.0(effect@3.19.15) + effect: 3.19.15 + '@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15)': dependencies: '@effect/platform': 0.94.2(effect@3.19.15) @@ -2444,10 +2571,14 @@ snapshots: effect: 3.19.15 uuid: 11.1.0 + '@effect/typeclass@0.38.0(effect@3.19.15)': + dependencies: + effect: 3.19.15 + '@effect/vitest@0.27.0(effect@3.19.15)(vitest@4.0.18)': dependencies: effect: 3.19.15 - vitest: 4.0.18(@types/node@25.0.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) '@effect/workflow@0.16.0(@effect/experimental@0.58.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(@effect/platform@0.94.2(effect@3.19.15))(@effect/rpc@0.73.0(@effect/platform@0.94.2(effect@3.19.15))(effect@3.19.15))(effect@3.19.15)': dependencies: @@ -2588,28 +2719,28 @@ snapshots: '@noble/hashes@1.8.0': {} - '@oxlint/darwin-arm64@1.41.0': + '@oxlint/darwin-arm64@1.42.0': optional: true - '@oxlint/darwin-x64@1.41.0': + '@oxlint/darwin-x64@1.42.0': optional: true - '@oxlint/linux-arm64-gnu@1.41.0': + '@oxlint/linux-arm64-gnu@1.42.0': optional: true - '@oxlint/linux-arm64-musl@1.41.0': + '@oxlint/linux-arm64-musl@1.42.0': optional: true - '@oxlint/linux-x64-gnu@1.41.0': + '@oxlint/linux-x64-gnu@1.42.0': optional: true - '@oxlint/linux-x64-musl@1.41.0': + '@oxlint/linux-x64-musl@1.42.0': optional: true - '@oxlint/win32-arm64@1.41.0': + '@oxlint/win32-arm64@1.42.0': optional: true - '@oxlint/win32-x64@1.41.0': + '@oxlint/win32-x64@1.42.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -2782,7 +2913,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@types/chai@5.2.3': dependencies: @@ -2791,7 +2922,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@types/deep-eql@4.0.2': {} @@ -2799,7 +2930,7 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -2815,7 +2946,7 @@ snapshots: '@types/mime@1.3.5': {} - '@types/node@25.0.10': + '@types/node@25.1.0': dependencies: undici-types: 7.16.0 @@ -2826,16 +2957,16 @@ snapshots: '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@types/send@1.2.1': dependencies: - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@types/send': 0.17.6 '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': @@ -2873,36 +3004,36 @@ snapshots: '@typescript-eslint/types': 8.50.0 eslint-visitor-keys: 4.2.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260122.4': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260122.4': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260122.4': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260122.4': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260122.4': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260122.4': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260122.4': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260130.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260122.4': + '@typescript/native-preview@7.0.0-dev.20260130.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260122.4 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260122.4 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260122.4 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260122.4 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260122.4 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260122.4 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260122.4 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260130.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260130.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260130.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260130.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260130.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260130.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260130.1 '@typescript/vfs@1.6.2(typescript@5.9.3)': dependencies: @@ -2923,7 +3054,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.0.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -2934,13 +3065,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -2968,7 +3099,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.0.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2) '@vitest/utils@4.0.18': dependencies: @@ -3083,6 +3214,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + caniuse-lite@1.0.30001766: {} chai@6.2.2: {} @@ -3137,10 +3272,19 @@ snapshots: deep-extend@0.6.0: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 + define-lazy-prop@3.0.0: {} + dependency-tree@11.2.0: dependencies: commander: 12.1.0 @@ -3372,7 +3516,7 @@ snapshots: kind-of: 6.0.3 which: 4.0.0 - globals@17.1.0: {} + globals@17.2.0: {} globrex@0.1.2: {} @@ -3412,12 +3556,20 @@ snapshots: dependencies: hasown: 2.0.2 + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@1.0.0: {} is-number@7.0.0: @@ -3433,6 +3585,10 @@ snapshots: is-url@1.2.4: {} + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isexe@3.1.1: {} isows@1.0.7(ws@8.18.3): @@ -3454,6 +3610,8 @@ snapshots: jiti@2.6.1: {} + jose@6.1.3: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -3597,6 +3755,15 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@11.0.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -3624,16 +3791,16 @@ snapshots: transitivePeerDependencies: - zod - oxlint@1.41.0: + oxlint@1.42.0: optionalDependencies: - '@oxlint/darwin-arm64': 1.41.0 - '@oxlint/darwin-x64': 1.41.0 - '@oxlint/linux-arm64-gnu': 1.41.0 - '@oxlint/linux-arm64-musl': 1.41.0 - '@oxlint/linux-x64-gnu': 1.41.0 - '@oxlint/linux-x64-musl': 1.41.0 - '@oxlint/win32-arm64': 1.41.0 - '@oxlint/win32-x64': 1.41.0 + '@oxlint/darwin-arm64': 1.42.0 + '@oxlint/darwin-x64': 1.42.0 + '@oxlint/linux-arm64-gnu': 1.42.0 + '@oxlint/linux-arm64-musl': 1.42.0 + '@oxlint/linux-x64-gnu': 1.42.0 + '@oxlint/linux-x64-musl': 1.42.0 + '@oxlint/win32-arm64': 1.42.0 + '@oxlint/win32-x64': 1.42.0 parse-ms@2.1.0: {} @@ -3672,6 +3839,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + precinct@12.2.0: dependencies: '@dependents/detective-less': 5.0.1 @@ -3769,6 +3938,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.56.0 fsevents: 2.3.3 + run-applescript@7.1.0: {} + safe-buffer@5.2.1: {} sass-lookup@6.1.0: @@ -3853,6 +4024,8 @@ snapshots: is-number: 7.0.0 optional: true + toml@3.0.0: {} + totalist@3.0.1: {} ts-api-utils@2.1.0(typescript@5.9.3): @@ -3901,7 +4074,7 @@ snapshots: uuid@11.1.0: {} - viem@2.44.4(typescript@5.9.3): + viem@2.45.1(typescript@5.9.3): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -3918,18 +4091,17 @@ snapshots: - utf-8-validate - zod - vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - optionalDependencies: - vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2): + vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -3938,7 +4110,7 @@ snapshots: rollup: 4.56.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.10 + '@types/node': 25.1.0 fsevents: 2.3.3 jiti: 2.6.1 yaml: 2.8.2 @@ -3947,10 +4119,10 @@ snapshots: dependencies: '@types/express': 4.17.25 - vitest@4.0.18(@types/node@25.0.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -3967,10 +4139,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.0.10 + '@types/node': 25.1.0 '@vitest/ui': 4.0.18(vitest@4.0.18) transitivePeerDependencies: - jiti @@ -4006,7 +4178,11 @@ snapshots: ws@8.19.0: {} + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + yallist@3.1.1: {} - yaml@2.8.2: - optional: true + yaml@2.8.2: {} diff --git a/tsconfig.json b/tsconfig.json index 81477f5..6e8add1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "include": [ "**/vitest.*.ts", "./packages/**/*.ts", - "./packages/*/test/**/*.ts" + "./packages/*/test/**/*.ts", + "./packages/*/package.json" ], "compilerOptions": { "rootDir": ".", @@ -17,7 +18,9 @@ // that would otherwise be considered cyclic dependencies (e.g. loading `@edgeandnode/amp` in the another test // suite where the package _also_ depends on `@edgeandnode/amp`). "@edgeandnode/amp": ["./packages/amp/src/index.ts"], - "@edgeandnode/amp/*": ["./packages/amp/src/*.ts"] + "@edgeandnode/amp/*": ["./packages/amp/src/*.ts"], + "@edgeandnode/amp-cli": ["./packages/cli/src/index.ts"], + "@edgeandnode/amp-cli/*": ["./packages/cli/src/*.ts"] }, "plugins": [{ "name": "@effect/language-service", diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 74499c4..fd972bc 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -4,6 +4,7 @@ "include": [], "references": [ { "path": "packages/amp" }, + { "path": "packages/cli" }, { "path": "packages/tools/oxc" } ] }