From bc8ca44ea41e6d28b5592db2999d1c32a1a71446 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Fri, 21 Nov 2025 11:53:54 -0500 Subject: [PATCH 1/3] feat(auth): use the optional service pattern for auth-dependent layers --- typescript/amp/src/AmpRegistry.ts | 16 ++--- typescript/amp/src/Auth.ts | 48 +++++---------- typescript/amp/src/Model.ts | 20 ++++++- typescript/amp/src/api/Admin.ts | 31 +++++++--- typescript/amp/src/cli/commands/auth/login.ts | 13 +++-- .../amp/src/cli/commands/auth/logout.ts | 2 +- typescript/amp/src/cli/commands/auth/token.ts | 27 ++++----- typescript/amp/src/cli/commands/build.ts | 10 ++-- typescript/amp/src/cli/commands/deploy.ts | 10 ++-- typescript/amp/src/cli/commands/dev.ts | 10 ++-- typescript/amp/src/cli/commands/publish.ts | 7 +-- typescript/amp/src/cli/commands/query.ts | 2 +- typescript/amp/src/cli/commands/register.ts | 4 +- typescript/amp/src/cli/commands/studio.ts | 58 +++++++++---------- typescript/amp/test/AmpRegistry.test.ts | 8 +-- 15 files changed, 134 insertions(+), 132 deletions(-) diff --git a/typescript/amp/src/AmpRegistry.ts b/typescript/amp/src/AmpRegistry.ts index e7cdbb471..9a0fbc6e0 100644 --- a/typescript/amp/src/AmpRegistry.ts +++ b/typescript/amp/src/AmpRegistry.ts @@ -76,7 +76,7 @@ export class AmpRegistryService extends Effect.Service()("Am const getRequest = ( url: string, responseSchema: Schema.Schema, - auth?: Auth.AuthStorageSchema, + auth?: Model.CachedAuthInfo, ): Effect.Effect, RegistryApiError, never> => { const request = HttpClientRequest.get(url, { acceptJson: true }) const authenticatedRequest = auth @@ -111,7 +111,7 @@ export class AmpRegistryService extends Effect.Service()("Am const makeAuthenticatedRequest = ( method: "POST" | "PUT", url: string, - auth: Auth.AuthStorageSchema, + auth: Model.CachedAuthInfo, bodySchema: Schema.Schema, body: I, responseSchema: Schema.Schema, @@ -180,7 +180,7 @@ export class AmpRegistryService extends Effect.Service()("Am * @returns Option.some(dataset) if found, Option.none() if not found */ const getOwnedDataset = ( - auth: Auth.AuthStorageSchema, + auth: Model.CachedAuthInfo, namespace: Model.DatasetNamespace, name: Model.DatasetName, ): Effect.Effect, RegistryApiError, never> => @@ -194,7 +194,7 @@ export class AmpRegistryService extends Effect.Service()("Am * @returns Option of dataset from either endpoint */ const getDatasetWithFallback = ( - auth: Auth.AuthStorageSchema, + auth: Model.CachedAuthInfo, namespace: Model.DatasetNamespace, name: Model.DatasetName, ): Effect.Effect, RegistryApiError, never> => @@ -214,7 +214,7 @@ export class AmpRegistryService extends Effect.Service()("Am * @returns Created dataset */ const publishDataset = ( - auth: Auth.AuthStorageSchema, + auth: Model.CachedAuthInfo, dto: AmpRegistryInsertDatasetDto, ): Effect.Effect => makeAuthenticatedRequest( @@ -235,7 +235,7 @@ export class AmpRegistryService extends Effect.Service()("Am * @returns Created version */ const publishVersion = ( - auth: Auth.AuthStorageSchema, + auth: Model.CachedAuthInfo, namespace: Model.DatasetNamespace, name: Model.DatasetName, dto: AmpRegistryInsertDatasetVersionDto, @@ -258,7 +258,7 @@ export class AmpRegistryService extends Effect.Service()("Am * @returns Updated dataset */ const updateDatasetMetadata = ( - auth: Auth.AuthStorageSchema, + auth: Model.CachedAuthInfo, namespace: Model.DatasetNamespace, name: Model.DatasetName, dto: AmpRegistryUpdateDatasetMetadataDto, @@ -340,7 +340,7 @@ export class AmpRegistryService extends Effect.Service()("Am */ const publishFlow = Effect.fn("DatasetPublishFlow")(function*( args: Readonly<{ - auth: Auth.AuthStorageSchema + auth: Model.CachedAuthInfo context: ManifestContext.DatasetContext versionTag: Model.DatasetRevision changelog?: string | undefined diff --git a/typescript/amp/src/Auth.ts b/typescript/amp/src/Auth.ts index 3ff7c023f..079d45b3a 100644 --- a/typescript/amp/src/Auth.ts +++ b/typescript/amp/src/Auth.ts @@ -43,7 +43,7 @@ export class GenerateAccessTokenRequest extends Schema.Class( "Amp/models/auth/GenerateAccessTokenResponse", )({ - token: Schema.NonEmptyTrimmedString, + token: Schema.Redacted(Schema.NonEmptyTrimmedString), token_type: Schema.Literal("Bearer"), exp: Schema.Int.pipe(Schema.positive()), sub: Schema.NonEmptyTrimmedString, @@ -60,17 +60,13 @@ export class GenerateAccessTokenError extends Data.TaggedError("Amp/errors/auth/ // Refresh Tokens // ============================================================================= -const AuthUserId = Schema.NonEmptyTrimmedString.pipe( - Schema.pattern(/^(c[a-z0-9]{24}|did:privy:c[a-z0-9]{24})$/), -) - export class RefreshTokenRequest extends Schema.Class( "Amp/models/auth/RefreshTokenRequest", )({ - refresh_token: Model.RefreshToken, - user_id: AuthUserId, + refresh_token: Schema.Redacted(Model.RefreshToken), + user_id: Model.AuthenticatedUserId, }) { - static fromCache(cache: AuthStorageSchema) { + static fromCache(cache: Model.CachedAuthInfo) { return RefreshTokenRequest.make({ user_id: cache.userId, refresh_token: cache.refreshToken, @@ -91,7 +87,7 @@ export class RefreshTokenResponse extends Schema.Class( description: "Seconds from receipt of when the token expires (def is 1hr)", }), user: Schema.Struct({ - id: AuthUserId, + id: Model.AuthenticatedUserId, accounts: Schema.Array(Schema.Union(Schema.NonEmptyTrimmedString, Model.Address)).annotations({ description: "List of accounts (connected wallets, etc) belonging to the user", examples: [["cmfd6bf6u006vjx0b7xb2eybx", "0x5c8fA0bDf68C915a88cD68291fC7CF011C126C29"]], @@ -101,18 +97,6 @@ export class RefreshTokenResponse extends Schema.Class( }), }) {} -// ============================================================================= -// Token Cache -// ============================================================================= - -export class AuthStorageSchema extends Schema.Class("Amp/models/auth/AuthStorageSchema")({ - accessToken: Model.AccessToken, - refreshToken: Model.RefreshToken, - userId: AuthUserId, - accounts: Schema.Array(Schema.Union(Schema.NonEmptyTrimmedString, Model.Address)).pipe(Schema.optional), - expiry: Schema.Int.pipe(Schema.positive(), Schema.optional), -}) {} - // ============================================================================= // Errors // ============================================================================= @@ -138,11 +122,11 @@ export class VerifySignedAccessTokenError extends Data.TaggedError("Amp/errors/a readonly cause: unknown }> {} -export class AuthService extends Effect.Service()("Amp/AuthService", { +export class Auth extends Effect.Service()("Amp/Auth", { dependencies: [LocalCache, FetchHttpClient.layer], effect: Effect.gen(function*() { const store = yield* KeyValueStore.KeyValueStore - const kvs = store.forSchema(AuthStorageSchema) + const kvs = store.forSchema(Model.CachedAuthInfo) // Setup the `${AUTH_PLATFORM_URL}/v1/auth` base URL for the HTTP client const v1AuthUrl = new URL("api/v1/auth", AUTH_PLATFORM_URL) @@ -157,7 +141,7 @@ export class AuthService extends Effect.Service()("Amp/AuthService" // ------------------------------------------------------------------------ const generateAccessToken = Effect.fn("AuthService.generateAccessToken")(function*(args: { - readonly storedAuth: AuthStorageSchema + readonly cache: Model.CachedAuthInfo readonly exp: Model.GenrateTokenDuration | undefined readonly audience: ReadonlyArray | null | undefined }) { @@ -169,7 +153,7 @@ export class AuthService extends Effect.Service()("Amp/AuthService" audience: args.audience ?? undefined, })), acceptJson: true, - }).pipe(HttpClientRequest.bearerToken(args.storedAuth.accessToken)) + }).pipe(HttpClientRequest.bearerToken(args.cache.accessToken)) return yield* httpClient.execute(request).pipe( Effect.flatMap(HttpClientResponse.matchStatus({ @@ -211,7 +195,7 @@ export class AuthService extends Effect.Service()("Amp/AuthService" ) const refreshAccessToken = Effect.fn("AuthService.refreshAccessToken")(function*( - cache: AuthStorageSchema, + cache: Model.CachedAuthInfo, ) { const request = HttpClientRequest.post("/refresh", { // Unsafely creating the JSON body is acceptable here as the `user_id` @@ -259,12 +243,12 @@ export class AuthService extends Effect.Service()("Amp/AuthService" const expiry = DateTime.toEpochMillis(DateTime.add(now, { seconds: response.expires_in })) const accessToken = Model.AccessToken.make(response.token) - const refreshToken = Model.RefreshToken.make(response.refresh_token ?? cache.refreshToken) - const refreshedAuth = AuthStorageSchema.make({ + const refreshToken = Model.RefreshToken.make(response.refresh_token ?? Redacted.value(cache.refreshToken)) + const refreshedAuth = Model.CachedAuthInfo.make({ userId: response.user.id, accounts: response.user.accounts, - accessToken, - refreshToken, + accessToken: Redacted.make(accessToken), + refreshToken: Redacted.make(refreshToken), expiry, }) @@ -315,7 +299,7 @@ export class AuthService extends Effect.Service()("Amp/AuthService" return cache }, Effect.option) - const setCache = Effect.fn("AuthService.setToken")(function*(cache: AuthStorageSchema) { + const setCache = Effect.fn("AuthService.setToken")(function*(cache: Model.CachedAuthInfo) { yield* kvs.set(AUTH_TOKEN_CACHE_KEY, cache) }) @@ -337,4 +321,4 @@ export class AuthService extends Effect.Service()("Amp/AuthService" }), }) {} -export const layer = AuthService.Default +export const layer = Auth.Default diff --git a/typescript/amp/src/Model.ts b/typescript/amp/src/Model.ts index 2482affd4..2a1da0826 100644 --- a/typescript/amp/src/Model.ts +++ b/typescript/amp/src/Model.ts @@ -5,13 +5,29 @@ import { isAddress } from "viem" export const Address = Schema.NonEmptyTrimmedString.pipe(Schema.filter((val) => isAddress(val))) +export const AuthenticatedUserId = Schema.NonEmptyTrimmedString.pipe( + Schema.pattern(/^(c[a-z0-9]{24}|did:privy:c[a-z0-9]{24})$/), +).annotations({ identifier: "AuthenticatedUserId" }) +export type AuthenticatedUserId = typeof AuthenticatedUserId.Type + export const AccessToken = Schema.NonEmptyTrimmedString.pipe( Schema.brand("AccessToken"), -) +).annotations({ identifier: "AccessToken" }) +export type AccessToken = typeof AccessToken.Type export const RefreshToken = Schema.NonEmptyTrimmedString.pipe( Schema.brand("RefreshToken"), -) +).annotations({ identifier: "RefreshToken" }) +export type RefreshToken = typeof RefreshToken.Type + +export const CachedAuthInfo = Schema.Struct({ + accessToken: Schema.Redacted(AccessToken), + refreshToken: Schema.Redacted(RefreshToken), + userId: AuthenticatedUserId, + accounts: Schema.optional(Schema.Array(Schema.Union(Schema.NonEmptyTrimmedString, Address))), + expiry: Schema.Int.pipe(Schema.positive(), Schema.optional), +}).annotations({ identifier: "CachedAuthInfo" }) +export type CachedAuthInfo = typeof CachedAuthInfo.Type export const Network = Schema.Lowercase.pipe( Schema.annotations({ diff --git a/typescript/amp/src/api/Admin.ts b/typescript/amp/src/api/Admin.ts index 9f8ad8f31..afdfe4bdc 100644 --- a/typescript/amp/src/api/Admin.ts +++ b/typescript/amp/src/api/Admin.ts @@ -10,8 +10,11 @@ import type * as HttpClientError from "@effect/platform/HttpClientError" import * as HttpClientRequest from "@effect/platform/HttpClientRequest" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import { constant } from "effect/Function" import * as Layer from "effect/Layer" +import * as Option from "effect/Option" import * as Schema from "effect/Schema" +import * as Auth from "../Auth.ts" import * as Model from "../Model.ts" import * as Error from "./Error.ts" @@ -675,14 +678,24 @@ export const make = Effect.fn(function*(url: string, options?: { * @param token - The bearer token. * @returns A layer for the admin api service. */ -export const layer = (url: string, token?: string) => - Effect.gen(function*() { - const api = yield* make(url, { - transformClient: token === undefined ? undefined : (client) => - client.pipe( - HttpClient.mapRequestInput(HttpClientRequest.setHeader("Authorization", `Bearer ${token}`)), +export const layer = (url: string) => + Layer.effect(Admin)( + Effect.gen(function*() { + const auth = yield* Effect.serviceOption(Auth.Auth) + + const transformClient = auth.pipe( + Option.map((auth) => + HttpClient.mapRequestEffect(Effect.fnUntraced(function*(request) { + const cache = yield* auth.getCache() + return Option.match(cache, { + onSome: (cache) => HttpClientRequest.bearerToken(request, cache.accessToken), + onNone: constant(request), + }) + })) ), - }) + Option.getOrUndefined, + ) - return api - }).pipe(Layer.effect(Admin), Layer.provide(FetchHttpClient.layer)) + return yield* make(url, { transformClient }) + }), + ).pipe(Layer.provide(FetchHttpClient.layer)) diff --git a/typescript/amp/src/cli/commands/auth/login.ts b/typescript/amp/src/cli/commands/auth/login.ts index 5e0cb6e42..b7612eedb 100644 --- a/typescript/amp/src/cli/commands/auth/login.ts +++ b/typescript/amp/src/cli/commands/auth/login.ts @@ -13,6 +13,7 @@ import * as Fiber from "effect/Fiber" import * as Fn from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" +import * as Redacted from "effect/Redacted" import * as Schedule from "effect/Schedule" import * as Schema from "effect/Schema" import * as Stream from "effect/Stream" @@ -130,7 +131,7 @@ const DeviceTokenPollingResponse = Schema.Union( ) const checkAlreadyAuthenticated = Effect.gen(function*() { - const auth = yield* Auth.AuthService + const auth = yield* Auth.Auth const authResult = yield* auth.getCache() return Option.isSome(authResult) @@ -331,7 +332,7 @@ const authenticate = Effect.fn("PerformCliAuthentication")(function*(alreadyAuth return yield* Effect.logInfo("amp cli already authenticated!") } - const auth = yield* Auth.AuthService + const auth = yield* Auth.Auth // Step 1: Request device authorization from the backend const { codeVerifier, deviceAuth } = yield* requestDeviceAuthorization() @@ -350,9 +351,11 @@ const authenticate = Effect.fn("PerformCliAuthentication")(function*(alreadyAuth // Step 4: Store the tokens const now = yield* DateTime.now const expiry = Fn.pipe(now, DateTime.add({ seconds: tokenResponse.expires_in }), DateTime.toEpochMillis) - yield* auth.setCache(Auth.AuthStorageSchema.make({ - accessToken: Model.AccessToken.make(tokenResponse.access_token), - refreshToken: Model.RefreshToken.make(tokenResponse.refresh_token), + const accessToken = Model.AccessToken.make(tokenResponse.access_token) + const refreshToken = Model.RefreshToken.make(tokenResponse.refresh_token) + yield* auth.setCache(Model.CachedAuthInfo.make({ + accessToken: Redacted.make(accessToken), + refreshToken: Redacted.make(refreshToken), userId: tokenResponse.user_id, accounts: tokenResponse.user_accounts, expiry, diff --git a/typescript/amp/src/cli/commands/auth/logout.ts b/typescript/amp/src/cli/commands/auth/logout.ts index 252481609..8fc15d7c2 100644 --- a/typescript/amp/src/cli/commands/auth/logout.ts +++ b/typescript/amp/src/cli/commands/auth/logout.ts @@ -15,7 +15,7 @@ const confirm = Prompt.confirm({ export const logout = Command.prompt("logout", Prompt.all([confirm]), ([confirm]) => Effect.gen(function*() { - const auth = yield* Auth.AuthService + const auth = yield* Auth.Auth if (!confirm) { return yield* Console.log("Exiting...") diff --git a/typescript/amp/src/cli/commands/auth/token.ts b/typescript/amp/src/cli/commands/auth/token.ts index 98990c2c6..56e2d35ba 100644 --- a/typescript/amp/src/cli/commands/auth/token.ts +++ b/typescript/amp/src/cli/commands/auth/token.ts @@ -31,7 +31,7 @@ export const token = Command.make("token", { ), Command.withHandler(({ args }) => Effect.gen(function*() { - const auth = yield* Auth.AuthService + const auth = yield* Auth.Auth const maybeAuthStorage = yield* auth.getCache() if (Option.isNone(maybeAuthStorage)) { @@ -42,7 +42,7 @@ export const token = Command.make("token", { const authStorage = maybeAuthStorage.value const response = yield* auth.generateAccessToken({ - storedAuth: authStorage, + cache: authStorage, exp: args.duration, audience: Option.getOrElse(args.audience, () => undefined), }).pipe( @@ -60,19 +60,14 @@ export const token = Command.make("token", { ) // verify the auth token against the JWKS - yield* auth - .verifySignedAccessToken( - Redacted.make(response.token), - response.iss, - ) - .pipe( - Effect.catchTag("Amp/errors/auth/VerifySignedAccessTokenError", (error) => - Console.error(`Failed to verify the signed token. JWT incorrectly formed: ${error.message}`).pipe( - Effect.flatMap(() => - ExitCode.NonZero - ), - )), - ) + yield* auth.verifySignedAccessToken(response.token, response.iss).pipe( + Effect.catchTag("Amp/errors/auth/VerifySignedAccessTokenError", (error) => + Console.error(`Failed to verify the signed token. JWT incorrectly formed: ${error.message}`).pipe( + Effect.flatMap(() => + ExitCode.NonZero + ), + )), + ) const exp = response.exp const expDateTime = DateTime.unsafeMake(exp * 1000) @@ -83,7 +78,7 @@ export const token = Command.make("token", { yield* Console.log( "Use this as a Authorization Bearer token in requests to query a published Amp Dataset to the Gateway", ) - yield* Console.log(` token:`, response.token) + yield* Console.log(` token:`, Redacted.value(response.token)) yield* Console.log(` exp:`, formatted) return yield* ExitCode.Zero }) diff --git a/typescript/amp/src/cli/commands/build.ts b/typescript/amp/src/cli/commands/build.ts index b5612a831..a3c49b5c0 100644 --- a/typescript/amp/src/cli/commands/build.ts +++ b/typescript/amp/src/cli/commands/build.ts @@ -44,11 +44,9 @@ export const build = Command.make("build", { }), ), Command.provide(({ args }) => - ManifestContext.layerFromConfigFile(args.config).pipe(Layer.provide( - Layer.unwrapEffect(Effect.gen(function*() { - const token = yield* Auth.AuthService.pipe(Effect.flatMap((auth) => auth.getCache())) - return Admin.layer(`${args.adminUrl}`, Option.getOrUndefined(token)?.accessToken) - })).pipe(Layer.provide(Auth.layer)), - )) + ManifestContext.layerFromConfigFile(args.config).pipe( + Layer.provide(Admin.layer(`${args.adminUrl}`)), + Layer.provide(Auth.layer), + ) ), ) diff --git a/typescript/amp/src/cli/commands/deploy.ts b/typescript/amp/src/cli/commands/deploy.ts index 9a9df607b..2872c86c6 100644 --- a/typescript/amp/src/cli/commands/deploy.ts +++ b/typescript/amp/src/cli/commands/deploy.ts @@ -51,11 +51,9 @@ export const deploy = Command.make("deploy", { }), ), Command.provide(({ args }) => - ManifestContext.layerFromConfigFile(args.configFile).pipe(Layer.provideMerge( - Layer.unwrapEffect(Effect.gen(function*() { - const token = yield* Auth.AuthService.pipe(Effect.flatMap((auth) => auth.getCache())) - return Admin.layer(`${args.adminUrl}`, Option.getOrUndefined(token)?.accessToken) - })).pipe(Layer.provide(Auth.layer)), - )) + ManifestContext.layerFromConfigFile(args.configFile).pipe( + Layer.provideMerge(Admin.layer(`${args.adminUrl}`)), + Layer.provide(Auth.layer), + ) ), ) diff --git a/typescript/amp/src/cli/commands/dev.ts b/typescript/amp/src/cli/commands/dev.ts index 710280298..ca5832300 100644 --- a/typescript/amp/src/cli/commands/dev.ts +++ b/typescript/amp/src/cli/commands/dev.ts @@ -47,11 +47,9 @@ export const dev = Command.make("dev", { args: { adminUrl } }).pipe( }), ), Command.provide(({ args }) => - ConfigLoader.ConfigLoader.Default.pipe(Layer.provideMerge( - Layer.unwrapEffect(Effect.gen(function*() { - const token = yield* Auth.AuthService.pipe(Effect.flatMap((auth) => auth.getCache())) - return Admin.layer(`${args.adminUrl}`, Option.getOrUndefined(token)?.accessToken) - })).pipe(Layer.provide(Auth.layer)), - )) + ConfigLoader.ConfigLoader.Default.pipe( + Layer.provideMerge(Admin.layer(`${args.adminUrl}`)), + Layer.provide(Auth.layer), + ) ), ) diff --git a/typescript/amp/src/cli/commands/publish.ts b/typescript/amp/src/cli/commands/publish.ts index e26c59af6..5bc12f5fb 100644 --- a/typescript/amp/src/cli/commands/publish.ts +++ b/typescript/amp/src/cli/commands/publish.ts @@ -36,7 +36,7 @@ export const publish = Command.make("publish", { Command.withHandler(({ args }) => Effect.gen(function*() { const context = yield* ManifestContext.ManifestContext - const auth = yield* Auth.AuthService + const auth = yield* Auth.Auth const ampRegistry = yield* AmpRegistry.AmpRegistryService const client = yield* Admin.Admin @@ -110,10 +110,7 @@ export const publish = Command.make("publish", { AmpRegistry.layer, ManifestContext.layerFromConfigFile(args.configFile), ).pipe( - Layer.provideMerge(Layer.unwrapEffect(Effect.gen(function*() { - const token = yield* Auth.AuthService.pipe(Effect.flatMap((auth) => auth.getCache())) - return Admin.layer(`${CLUSTER_ADMIN_URL}`, Option.getOrUndefined(token)?.accessToken) - }))), + Layer.provideMerge(Admin.layer(`${CLUSTER_ADMIN_URL}`)), Layer.provideMerge(Auth.layer), ) ), diff --git a/typescript/amp/src/cli/commands/query.ts b/typescript/amp/src/cli/commands/query.ts index f2e159b7b..3ab78ff5b 100644 --- a/typescript/amp/src/cli/commands/query.ts +++ b/typescript/amp/src/cli/commands/query.ts @@ -86,7 +86,7 @@ export const query = Command.make("query", { ), Command.provide(({ args }) => Layer.unwrapEffect(Effect.gen(function*() { - const auth = yield* Auth.AuthService + const auth = yield* Auth.Auth const maybeToken: Option.Option = yield* args.bearerToken.pipe( Option.match({ diff --git a/typescript/amp/src/cli/commands/register.ts b/typescript/amp/src/cli/commands/register.ts index c343249e5..ded9e2142 100644 --- a/typescript/amp/src/cli/commands/register.ts +++ b/typescript/amp/src/cli/commands/register.ts @@ -37,6 +37,8 @@ export const register = Command.make("register", { }), ), Command.provide(({ args }) => - ManifestContext.layerFromConfigFile(args.configFile).pipe(Layer.provideMerge(Admin.layer(`${args.adminUrl}`))) + ManifestContext.layerFromConfigFile(args.configFile).pipe( + Layer.provideMerge(Admin.layer(`${args.adminUrl}`)), + ) ), ) diff --git a/typescript/amp/src/cli/commands/studio.ts b/typescript/amp/src/cli/commands/studio.ts index 5cea42cfe..5da8fac92 100644 --- a/typescript/amp/src/cli/commands/studio.ts +++ b/typescript/amp/src/cli/commands/studio.ts @@ -522,36 +522,34 @@ export const studio = Command.make("studio", { }).pipe( Command.withDescription("Opens the amp dataset studio visualization tool"), Command.withHandler(({ args }) => - Effect.gen(function*() { - yield* Server.pipe( - HttpServer.withLogAddress, - Layer.provide(NodeHttpServer.layer(createServer, { port: args.port })), - Layer.tap(() => - Effect.gen(function*() { - if (args.open) { - return yield* openBrowser(args.port, args.browser).pipe( - Effect.tapErrorCause((cause) => - Console.warn( - `Failure opening amp dataset studio in your browser. Open at http://localhost:${args.port}`, - { - cause, - }, - ) - ), - Effect.orElseSucceed(() => Effect.void), - ) - } - return Effect.void - }) - ), - Layer.tap(() => - Console.log( - `🎉 amp dataset studio started and running at http://localhost:${args.port}`, - ) - ), - Layer.launch, - ) - }) + Server.pipe( + HttpServer.withLogAddress, + Layer.provide(NodeHttpServer.layer(createServer, { port: args.port })), + Layer.tap(() => + Effect.gen(function*() { + if (args.open) { + return yield* openBrowser(args.port, args.browser).pipe( + Effect.tapErrorCause((cause) => + Console.warn( + `Failure opening amp dataset studio in your browser. Open at http://localhost:${args.port}`, + { + cause, + }, + ) + ), + Effect.orElseSucceed(() => Effect.void), + ) + } + return yield* Effect.void + }) + ), + Layer.tap(() => + Console.log( + `🎉 amp dataset studio started and running at http://localhost:${args.port}`, + ) + ), + Layer.launch, + ) ), Command.provide(ConfigLoader.ConfigLoader.Default), Command.provide(FoundryQueryableEventResolver.layer), diff --git a/typescript/amp/test/AmpRegistry.test.ts b/typescript/amp/test/AmpRegistry.test.ts index 6f724366f..f41d08383 100644 --- a/typescript/amp/test/AmpRegistry.test.ts +++ b/typescript/amp/test/AmpRegistry.test.ts @@ -6,17 +6,17 @@ import { afterEach, describe, it } from "@effect/vitest" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Option from "effect/Option" +import * as Redacted from "effect/Redacted" import * as AmpRegistry from "@edgeandnode/amp/AmpRegistry" -import * as Auth from "@edgeandnode/amp/Auth" import type * as ManifestContext from "@edgeandnode/amp/ManifestContext" import * as Model from "@edgeandnode/amp/Model" // Test Fixtures -const mockAuthStorage = Auth.AuthStorageSchema.make({ - accessToken: Model.AccessToken.make("test-access-token"), - refreshToken: Model.RefreshToken.make("test-refresh-token"), +const mockAuthStorage = Model.CachedAuthInfo.make({ + accessToken: Redacted.make(Model.AccessToken.make("test-access-token")), + refreshToken: Redacted.make(Model.RefreshToken.make("test-refresh-token")), userId: "cmfoby1bt005el70b0fjd3glv", accounts: ["cmfoby1bt005el70b0fjd3glv", "0x04913E13A937cf63Fad3786FEE42b3d44dA558aA"], expiry: Date.now() + 3600000, From b2a38b84341852ddb5454d6dc02fe1990fc1ad6a Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Fri, 21 Nov 2025 12:11:58 -0500 Subject: [PATCH 2/3] fix jsdoc for Admin.layer --- typescript/amp/src/api/Admin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typescript/amp/src/api/Admin.ts b/typescript/amp/src/api/Admin.ts index afdfe4bdc..ae1f8df15 100644 --- a/typescript/amp/src/api/Admin.ts +++ b/typescript/amp/src/api/Admin.ts @@ -675,7 +675,6 @@ export const make = Effect.fn(function*(url: string, options?: { * Creates a layer for the admin api service. * * @param url - The url of the admin api service. - * @param token - The bearer token. * @returns A layer for the admin api service. */ export const layer = (url: string) => From d1d677dc5e0b1a32a29b643936f843983c422781 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Fri, 21 Nov 2025 12:16:33 -0500 Subject: [PATCH 3/3] remove need for URL interpolation --- typescript/amp/src/api/Admin.ts | 4 ++-- typescript/amp/src/cli/commands/build.ts | 2 +- typescript/amp/src/cli/commands/deploy.ts | 2 +- typescript/amp/src/cli/commands/dev.ts | 2 +- typescript/amp/src/cli/commands/proxy.ts | 2 +- typescript/amp/src/cli/commands/publish.ts | 2 +- typescript/amp/src/cli/commands/register.ts | 2 +- typescript/amp/src/cli/commands/studio.ts | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/typescript/amp/src/api/Admin.ts b/typescript/amp/src/api/Admin.ts index ae1f8df15..350c89a56 100644 --- a/typescript/amp/src/api/Admin.ts +++ b/typescript/amp/src/api/Admin.ts @@ -490,7 +490,7 @@ export class Admin extends Context.Tag("Amp/Admin") HttpClient.HttpClient) | undefined readonly transformResponse?: | ((effect: Effect.Effect) => Effect.Effect) @@ -677,7 +677,7 @@ export const make = Effect.fn(function*(url: string, options?: { * @param url - The url of the admin api service. * @returns A layer for the admin api service. */ -export const layer = (url: string) => +export const layer = (url: string | URL) => Layer.effect(Admin)( Effect.gen(function*() { const auth = yield* Effect.serviceOption(Auth.Auth) diff --git a/typescript/amp/src/cli/commands/build.ts b/typescript/amp/src/cli/commands/build.ts index a3c49b5c0..30a8e1da4 100644 --- a/typescript/amp/src/cli/commands/build.ts +++ b/typescript/amp/src/cli/commands/build.ts @@ -45,7 +45,7 @@ export const build = Command.make("build", { ), Command.provide(({ args }) => ManifestContext.layerFromConfigFile(args.config).pipe( - Layer.provide(Admin.layer(`${args.adminUrl}`)), + Layer.provide(Admin.layer(args.adminUrl)), Layer.provide(Auth.layer), ) ), diff --git a/typescript/amp/src/cli/commands/deploy.ts b/typescript/amp/src/cli/commands/deploy.ts index 2872c86c6..d4c541791 100644 --- a/typescript/amp/src/cli/commands/deploy.ts +++ b/typescript/amp/src/cli/commands/deploy.ts @@ -52,7 +52,7 @@ export const deploy = Command.make("deploy", { ), Command.provide(({ args }) => ManifestContext.layerFromConfigFile(args.configFile).pipe( - Layer.provideMerge(Admin.layer(`${args.adminUrl}`)), + Layer.provideMerge(Admin.layer(args.adminUrl)), Layer.provide(Auth.layer), ) ), diff --git a/typescript/amp/src/cli/commands/dev.ts b/typescript/amp/src/cli/commands/dev.ts index ca5832300..6c87cf657 100644 --- a/typescript/amp/src/cli/commands/dev.ts +++ b/typescript/amp/src/cli/commands/dev.ts @@ -48,7 +48,7 @@ export const dev = Command.make("dev", { args: { adminUrl } }).pipe( ), Command.provide(({ args }) => ConfigLoader.ConfigLoader.Default.pipe( - Layer.provideMerge(Admin.layer(`${args.adminUrl}`)), + Layer.provideMerge(Admin.layer(args.adminUrl)), Layer.provide(Auth.layer), ) ), diff --git a/typescript/amp/src/cli/commands/proxy.ts b/typescript/amp/src/cli/commands/proxy.ts index 8fde84308..899be0560 100644 --- a/typescript/amp/src/cli/commands/proxy.ts +++ b/typescript/amp/src/cli/commands/proxy.ts @@ -50,5 +50,5 @@ export const proxy = Command.make("proxy", { yield* Effect.acquireRelease(acquire, release).pipe(Effect.zip(Effect.never)) }, Effect.scoped), ), - Command.provide(({ args }) => ArrowFlight.layer(createGrpcTransport({ baseUrl: `${args.flightUrl}` }))), + Command.provide(({ args }) => ArrowFlight.layer(createGrpcTransport({ baseUrl: args.flightUrl.toString() }))), ) diff --git a/typescript/amp/src/cli/commands/publish.ts b/typescript/amp/src/cli/commands/publish.ts index 5bc12f5fb..137902775 100644 --- a/typescript/amp/src/cli/commands/publish.ts +++ b/typescript/amp/src/cli/commands/publish.ts @@ -110,7 +110,7 @@ export const publish = Command.make("publish", { AmpRegistry.layer, ManifestContext.layerFromConfigFile(args.configFile), ).pipe( - Layer.provideMerge(Admin.layer(`${CLUSTER_ADMIN_URL}`)), + Layer.provideMerge(Admin.layer(CLUSTER_ADMIN_URL)), Layer.provideMerge(Auth.layer), ) ), diff --git a/typescript/amp/src/cli/commands/register.ts b/typescript/amp/src/cli/commands/register.ts index ded9e2142..08cd96419 100644 --- a/typescript/amp/src/cli/commands/register.ts +++ b/typescript/amp/src/cli/commands/register.ts @@ -38,7 +38,7 @@ export const register = Command.make("register", { ), Command.provide(({ args }) => ManifestContext.layerFromConfigFile(args.configFile).pipe( - Layer.provideMerge(Admin.layer(`${args.adminUrl}`)), + Layer.provideMerge(Admin.layer(args.adminUrl)), ) ), ) diff --git a/typescript/amp/src/cli/commands/studio.ts b/typescript/amp/src/cli/commands/studio.ts index 5da8fac92..cd1b14d68 100644 --- a/typescript/amp/src/cli/commands/studio.ts +++ b/typescript/amp/src/cli/commands/studio.ts @@ -553,8 +553,8 @@ export const studio = Command.make("studio", { ), Command.provide(ConfigLoader.ConfigLoader.Default), Command.provide(FoundryQueryableEventResolver.layer), - Command.provide(({ args }) => Admin.layer(`${args.adminUrl}`)), - Command.provide(({ args }) => ArrowFlight.layer(createGrpcTransport({ baseUrl: `${args.flightUrl}` }))), + Command.provide(({ args }) => Admin.layer(args.adminUrl)), + Command.provide(({ args }) => ArrowFlight.layer(createGrpcTransport({ baseUrl: args.flightUrl.toString() }))), ) const openBrowser = (