everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
225 lines (223 loc) • 9.23 kB
JavaScript
import { getNetworkIdForAccount } from "../utils/network.mjs";
import { colors } from "../utils/theme.mjs";
import { DEFAULT_DEPLOY_KEY_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_EXPIRES_IN_SECONDS } from "../contract.mjs";
import { ResolutionSessionService } from "../services/resolution/session.mjs";
import { SiteClient } from "../services/auth/site-client.mjs";
import { DeviceLoginService, openInBrowser } from "../services/auth/device-login.mjs";
import { loginOutlayerCli } from "../services/auth/outlayer.mjs";
import { mintPublishKey } from "../services/auth/publish-key.mjs";
import { SessionStore } from "../services/auth/session-store.mjs";
import { Effect } from "effect";
import process from "node:process";
import { parseKey } from "near-kit";
//#region src/handlers/keys.ts
function registerKeys(builder) {
return {
login: builder.login.effect(function* (_opts, input) {
const session = yield* ResolutionSessionService;
const deviceLogin = yield* DeviceLoginService;
const sessions = yield* SessionStore;
const staging = input.env === "staging";
const config = session?.config;
const account = staging ? config?.staging?.account ?? config?.account ?? "" : config?.account ?? "";
const gateway = staging ? config?.staging?.domain ?? config?.domain ?? "" : config?.domain ?? "";
const expiresIn = input.expiresIn ?? (input.key ? 31536e3 : 7776e3);
const siteUrl = resolveLoginSiteUrl(input.site, session, staging);
let deployKey;
if (input.key) {
if (!account || !gateway) return {
status: "error",
siteUrl,
error: "Deploy keys scope to the authored config's account and domain — set both in bos.config.json (or drop --key for a plain login)"
};
deployKey = {
account,
gateway
};
}
const login = yield* deviceLogin.start({
siteUrl,
device: input.device,
account: account || void 0,
custody: input.custody ? { network: getNetworkIdForAccount(account) } : void 0,
expiresIn,
deployKey
}).pipe(Effect.catchIf(() => true, (error) => Effect.succeed({ startFailed: error.message })));
if ("startFailed" in login) return {
status: "error",
siteUrl: input.site ?? "",
error: login.startFailed
};
console.log();
console.log(` One-time code: ${colors.cyan(login.userCode)}`);
yield* Effect.catchIf(Effect.tryPromise(() => openInBrowser(login.verificationUrl)), () => true, (error) => Effect.sync(() => console.log(colors.yellow(` ⚠ ${error.message}`))));
console.log();
console.log(` Waiting for approval at ${colors.dim(login.verificationUrl)}…`);
return yield* Effect.tryPromise({
try: async () => {
const approval = await login.waitForApproval();
if (!approval.apiKey) return {
status: "error",
siteUrl,
loginUrl: login.verificationUrl,
error: "Login approved but no CLI credential was created"
};
const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
const configDir = session?.root ?? process.cwd();
const existingSession = sessions.read(configDir);
sessions.write(configDir, {
version: 1,
credential: {
kind: "session",
apiKey: approval.apiKey.key,
apiKeyId: approval.apiKey.id,
accountId: approval.accountId,
label: input.device ?? siteUrl,
siteUrl,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
expiresAt
},
publishKey: null,
delegateKey: null,
custodyWallet: approval.custodyWallet ?? existingSession?.custodyWallet ?? null
});
let warning = null;
if (input.custody && !approval.custodyWallet) warning = "Custody wallet credential could not be fetched after approval. Re-run `bos login --custody` — the wallet is kept server-side.";
if (account && approval.accountId && approval.accountId !== account) warning = warning ? `${warning} Logged in as ${approval.accountId}, but the authored config account is ${account}. Publishes will use the configured account.` : `Logged in as ${approval.accountId}, but the authored config account is ${account}. Publishes will use the configured account.`;
let publishKey = null;
if (input.key) try {
publishKey = await exportPublishKey(sessions, account, input.registry);
} catch (error) {
warning = `Session stored, but publish-key export failed: ${error instanceof Error ? error.message : "unknown error"}`;
}
let outlayerCli = null;
const custodyWallet = approval.custodyWallet;
if (input.custody && custodyWallet) {
outlayerCli = await loginOutlayerCli(custodyWallet);
if (outlayerCli.status === "error") {
const failure = `OutLayer CLI login failed: ${outlayerCli.detail}`;
warning = warning ? `${warning} ${failure}` : failure;
}
}
return {
status: "logged-in",
siteUrl,
accountId: approval.accountId,
expiresAt,
loginUrl: login.verificationUrl,
publishKey,
deployKey: deployKey && approval.apiKey ? {
key: approval.apiKey.key,
account: deployKey.account,
gateway: deployKey.gateway
} : null,
custodyWallet: approval.custodyWallet ? {
walletId: approval.custodyWallet.walletId,
nearAccountId: approval.custodyWallet.nearAccountId,
network: approval.custodyWallet.network,
handoffUrl: approval.custodyWallet.handoffUrl
} : null,
outlayerCli,
warning
};
},
catch: (error) => ({
status: "error",
siteUrl: input.site ?? "",
error: error instanceof Error ? error.message : "Unknown error"
})
});
}),
logout: builder.logout.effect(function* (_opts, input) {
const session = yield* ResolutionSessionService;
const siteClient = yield* SiteClient;
const sessions = yield* SessionStore;
const configDir = input.configDir ?? session?.root ?? process.cwd();
return yield* Effect.tryPromise({
try: () => logoutBody({
siteClient,
sessions,
session
}, configDir),
catch: (error) => ({
status: "error",
revokedApiKey: false,
removedPublishKey: false,
error: error instanceof Error ? error.message : "Unknown error"
})
});
})
};
}
async function logoutBody(deps, configDir) {
const { siteClient, sessions, session } = deps;
const stored = sessions.read(configDir);
let revokedApiKey = false;
let removedPublishKey = false;
let warning = null;
const credential = stored?.credential;
if (credential) {
revokedApiKey = await revokeApiKey(siteClient, credential).catch(() => false);
if (!revokedApiKey) warning = "Could not revoke the API key remotely — revoke it manually under Settings → API keys.";
}
if (stored?.publishKey) {
const account = stored.credential?.accountId ?? session?.config?.account ?? "";
sessions.removePublishedKeyFile(stored.publishKey.network, account, stored.publishKey.publicKey);
removedPublishKey = true;
warning = warning ? `${warning} Re-run ${colors.cyan("bos login --key")} to re-export a publish key.` : "Removed the exported publish key. Re-run bos login --key to re-export one.";
}
sessions.delete(configDir);
return {
status: "logged-out",
revokedApiKey,
removedPublishKey,
warning
};
}
function resolveLoginSiteUrl(siteInput, session, staging) {
if (siteInput) return siteInput.replace(/\/+$/, "");
if (staging) {
const stagingDomain = session?.config?.staging?.domain ?? session?.config?.domain;
return stagingDomain ? `https://${stagingDomain}` : session?.runtime?.ui?.url ?? "";
}
const devUiUrl = session?.runtime?.ui?.url;
if (devUiUrl && /^https?:\/\/(localhost|127\.0\.0\.1)/.test(devUiUrl)) return devUiUrl.replace(/\/+$/, "");
const domain = session?.config?.domain;
return domain ? `https://${domain}` : "";
}
/** `login --key`'s export: the shared mint, then the near-cli-rs keystore
* handoff (~/.near-credentials) with a 0600 permission fix-up. */
async function exportPublishKey(sessions, account, registry) {
const minted = await mintPublishKey({
account,
registry
});
const { FileKeyStore } = await import("near-kit/keys/file");
await new FileKeyStore("~/.near-credentials", minted.network).add(account, parseNearPrivateKey(minted.keyPair.privateKey));
try {
const { statSync, chmodSync } = await import("node:fs");
const credentialPath = sessions.nearCredentialsPath(minted.network, account);
if (statSync(credentialPath).mode & 63) chmodSync(credentialPath, 384);
} catch {}
return {
publicKey: minted.keyPair.publicKey,
network: minted.network,
contract: minted.contract,
exportedTo: sessions.nearCredentialsPath(minted.network, account)
};
}
function parseNearPrivateKey(privateKey) {
return parseKey(privateKey);
}
async function revokeApiKey(siteClient, credential) {
if (!credential.apiKey || !credential.siteUrl) return false;
try {
const response = await siteClient.authPost(credential.siteUrl, "/api-key/delete", { keyId: credential.apiKeyId }, void 0, { "x-api-key": credential.apiKey }, AbortSignal.timeout(1e4));
return response.status >= 200 && response.status < 300;
} catch {
return false;
}
}
//#endregion
export { registerKeys };
//# sourceMappingURL=keys.mjs.map