From 1079c77d2e2d9843167a2ca647bd69c9a4966bdf Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Fri, 17 Oct 2025 18:34:52 +0200 Subject: [PATCH 01/23] chore: intro of fp-cloud-connector --- .gitignore | 1 + dashboard/fp-cloud-connector-test.html | 13 ++ .../fp-cloud-connector/fp-cloud-connector.ts | 19 +++ dashboard/fp-cloud-connector/fpcc-protocol.ts | 115 +++++++++++++ .../iframe-fpcc-protocol.ts | 33 ++++ .../fp-cloud-connector/injected-iframe.html | 9 ++ .../page-fpcc-protocol.test.ts | 45 ++++++ .../fp-cloud-connector/page-fpcc-protocol.ts | 45 ++++++ dashboard/fp-cloud-connector/page-handler.ts | 85 ++++++++++ dashboard/fp-cloud-connector/post-messager.ts | 135 ++++++++++++++++ .../protocol-fp-cloud-conn.ts | 151 ++++++++++++++++++ dashboard/package.json | 1 + dashboard/vite.config.ts | 34 +++- pnpm-lock.yaml | 31 ++++ 14 files changed, 713 insertions(+), 4 deletions(-) create mode 100644 dashboard/fp-cloud-connector-test.html create mode 100644 dashboard/fp-cloud-connector/fp-cloud-connector.ts create mode 100644 dashboard/fp-cloud-connector/fpcc-protocol.ts create mode 100644 dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts create mode 100644 dashboard/fp-cloud-connector/injected-iframe.html create mode 100644 dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts create mode 100644 dashboard/fp-cloud-connector/page-fpcc-protocol.ts create mode 100644 dashboard/fp-cloud-connector/page-handler.ts create mode 100644 dashboard/fp-cloud-connector/post-messager.ts create mode 100644 dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts diff --git a/.gitignore b/.gitignore index 05a79cfca..46bd52836 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Files +.claude/ .vscode .idea .DS_Store diff --git a/dashboard/fp-cloud-connector-test.html b/dashboard/fp-cloud-connector-test.html new file mode 100644 index 000000000..7d6d83d30 --- /dev/null +++ b/dashboard/fp-cloud-connector-test.html @@ -0,0 +1,13 @@ + + + + + + + Interactive Test Fireproof Cloud Connector + + +

Interactive Test Fireproof Cloud Connector

+ + + diff --git a/dashboard/fp-cloud-connector/fp-cloud-connector.ts b/dashboard/fp-cloud-connector/fp-cloud-connector.ts new file mode 100644 index 000000000..267f8b357 --- /dev/null +++ b/dashboard/fp-cloud-connector/fp-cloud-connector.ts @@ -0,0 +1,19 @@ +import { ensureSuperThis } from "@fireproof/core-runtime"; +import { Lazy } from "@adviser/cement"; +import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; +import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; + +const postMessager = Lazy(() => { + (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { + FP_DEBUG: "*", + }; + const sthis = ensureSuperThis(); + const protocol = new IframeFPCCProtocol(sthis); + window.addEventListener("message", protocol.handleMessage); + protocol.start((event: FPCCMessage, srcEvent: MessageEvent) => { + srcEvent.source?.postMessage(event, { targetOrigin: srcEvent.origin }); + }); + return protocol; +}); + +postMessager(); diff --git a/dashboard/fp-cloud-connector/fpcc-protocol.ts b/dashboard/fp-cloud-connector/fpcc-protocol.ts new file mode 100644 index 000000000..6c5352803 --- /dev/null +++ b/dashboard/fp-cloud-connector/fpcc-protocol.ts @@ -0,0 +1,115 @@ +import { SuperThis } from "use-fireproof"; +import { FPCCMessage, FPCCMsgBase, FPCCPong, FPCCSendMessage, isFPCCPing, validateFPCCMessage } from "./protocol-fp-cloud-conn.js"; +import { Logger } from "@adviser/cement"; +import { ensureLogger } from "@fireproof/core-runtime"; + +export interface FPCCProtocol { + // handle must be this bound method + handleMessage: (event: MessageEvent) => void; + handleFPCCMessage?: (event: FPCCMessage, srcEvent: MessageEvent) => void; + sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void; + handleError: (error: unknown) => void; + start(send: (evt: FPCCMessage, srcEvent: MessageEvent) => void): void; +} + +export class FPCCProtocolBase implements FPCCProtocol { + protected readonly sthis: SuperThis; + protected readonly logger: Logger; + + constructor(sthis: SuperThis, logger?: Logger) { + this.sthis = sthis; + this.logger = logger || ensureLogger(sthis, "FPCCProtocolBase"); + } + + handleMessage = (event: MessageEvent) => { + if ((event.data as { type: string })?.type === "EXTENSION_VERSION") { + // ignore extension version messages + return; + } + const fpCCmsg = validateFPCCMessage(event.data); + console.log("IframeFPCCProtocol handleMessage called", event.data, fpCCmsg.success); + if (fpCCmsg.success) { + this.handleFPCCMessage(fpCCmsg.data, event); + } else { + this.logger.Warn().Err(fpCCmsg.error).Any("event", event).Msg("Received non-FPCC message"); + } + }; + + #fpccMessageHandlers: ((msg: FPCCMessage) => boolean | undefined)[] = []; + onFPCCMessage(callback: (msg: FPCCMessage) => boolean | undefined): void { + this.#fpccMessageHandlers.push(callback); + } + + handleFPCCMessage = (event: FPCCMessage, srcEvent: MessageEvent) => { + // allow handlers to process the message first and abort further processing + if (this.#fpccMessageHandlers.map((handler) => handler(event)).some((handled) => handled)) { + return; + } + this.logger.Debug().Any("event", event).Msg("Handling FPCC message"); + switch (true) { + case isFPCCPing(event): { + this.sendMessage( + { + type: "FPCCPong", + dst: event.src, + pingTid: event.tid, + timestamp: Date.now(), + }, + srcEvent, + ); + break; + } + } + }; + + handleError = (_error: unknown) => { + throw new Error("Method not implemented."); + }; + + #sendFn?: (msg: FPCCMessage, srcEvent: MessageEvent) => void; + start(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent) => void): void { + this.#sendFn = sendFn; + } + + sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent): void { + if (!this.#sendFn) { + throw new Error("Protocol not started. Call start() before sending messages."); + } + this.#sendFn( + { + ...msg, + src: msg.src ?? srcEvent.origin ?? "src-unknown", + tid: msg.tid ?? this.sthis.nextId().str, + } as FPCCMessage, + srcEvent, + ); + } +} + +// this.listener = (event: MessageEvent) => { +// try { +// // Check origin if whitelist is provided +// if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) { +// if (!this.config.allowedOrigins.includes(event.origin)) { +// // eslint-disable-next-line no-console +// console.warn(`Message from unauthorized origin: ${event.origin}`); +// return; +// } +// } + +// if (!this.isMessageEvent(event)) { +// throw this.logger.Error().Any({ data: event.data }).Msg("Received message with invalid data structure(T)"); +// } + +// // Call the message handler +// this.config.onMessage(event.data, event); +// } catch (error) { +// const err = error instanceof Error ? error : new Error(String(error)); +// if (this.config.onError) { +// this.config.onError(err, event); +// } else { +// // eslint-disable-next-line no-console +// console.error("Error handling message:", err); +// } +// } +// }; diff --git a/dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts b/dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts new file mode 100644 index 000000000..786ae01d6 --- /dev/null +++ b/dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts @@ -0,0 +1,33 @@ +import { ensureLogger } from "@fireproof/core-runtime"; +import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; +import { FPCCMessage, FPCCMsgBase, FPCCSendMessage } from "./protocol-fp-cloud-conn.js"; +import { SuperThis } from "@fireproof/core-types-base"; +import { Logger } from "@adviser/cement"; + +export class IframeFPCCProtocol implements FPCCProtocol { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly fpccProtocol: FPCCProtocolBase; + + constructor(sthis: SuperThis) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "IframeFPCCProtocol"); + this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); + } + + readonly handleMessage = (event: MessageEvent): void => { + this.fpccProtocol.handleMessage(event); + }; + + readonly handleError = (_error: unknown): void => { + throw new Error("Method not implemented."); + }; + + start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => void): void { + this.fpccProtocol.start(sendFn); + } + + sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): void { + this.fpccProtocol.sendMessage(message, srcEvent); + } +} diff --git a/dashboard/fp-cloud-connector/injected-iframe.html b/dashboard/fp-cloud-connector/injected-iframe.html new file mode 100644 index 000000000..b0a10ed38 --- /dev/null +++ b/dashboard/fp-cloud-connector/injected-iframe.html @@ -0,0 +1,9 @@ + + + Fireproof Cloud Connector + + + I'm the Fireproof Cloud Connector + + + diff --git a/dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts b/dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts new file mode 100644 index 000000000..7ba5af97e --- /dev/null +++ b/dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from "vitest"; +import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; +import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; +import { FPCCMessage, FPCCPing } from "./protocol-fp-cloud-conn.js"; +import { ensureSuperThis } from "@fireproof/core-runtime"; + +describe("FPCC Protocol", () => { + const sthis = ensureSuperThis(); + const pageProtocol = new PageFPCCProtocol(sthis); + const iframeProtocol = new IframeFPCCProtocol(sthis); + + iframeProtocol.start((evt: FPCCMessage) => { + pageProtocol.handleMessage({ data: evt, origin: "page" } as MessageEvent); + }); + + function pageProtocolStart() { + pageProtocol.start((evt: FPCCMessage) => { + iframeProtocol.handleMessage({ data: evt, origin: "iframe" } as MessageEvent); + }); + } + + it("ping-pong", () => { + const pingMessage: FPCCPing = { + tid: "test-ping-1", + type: "FPCCPing", + src: "page", + dst: "iframe", + timestamp: Date.now(), + }; + const fpccFn = vi.fn(); + pageProtocol.onFPCCMessage(fpccFn); + pageProtocolStart(); + pageProtocol.sendMessage(pingMessage, {} as MessageEvent); + expect(fpccFn.mock.calls[fpccFn.mock.calls.length - 1]).toEqual([ + { + dst: "page", + pingTid: "test-ping-1", + src: "iframe", + tid: expect.any(String), + timestamp: expect.any(Number), + type: "FPCCPong", + }, + ]); + }); +}); diff --git a/dashboard/fp-cloud-connector/page-fpcc-protocol.ts b/dashboard/fp-cloud-connector/page-fpcc-protocol.ts new file mode 100644 index 000000000..988682292 --- /dev/null +++ b/dashboard/fp-cloud-connector/page-fpcc-protocol.ts @@ -0,0 +1,45 @@ +import { ensureLogger } from "@fireproof/core-runtime"; +import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; +import { SuperThis } from "@fireproof/core-types-base"; +import { Logger } from "@adviser/cement"; +import { FPCCMessage, FPCCMsgBase, FPCCPing, FPCCSendMessage } from "./protocol-fp-cloud-conn.js"; + +export class PageFPCCProtocol implements FPCCProtocol { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly fpccProtocol: FPCCProtocolBase; + + constructor(sthis: SuperThis) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "PageFPCCProtocol"); + this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); + } + + readonly handleMessage = (_event: MessageEvent): void => { + this.fpccProtocol.handleMessage(_event); + }; + + onFPCCMessage(callback: (msg: FPCCMessage) => boolean | undefined): void { + this.fpccProtocol.onFPCCMessage(callback); + } + + readonly handleError = (_error: unknown): void => { + throw new Error("Method not implemented."); + }; + + start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => void): void { + this.fpccProtocol.start(sendFn); + this.fpccProtocol.sendMessage( + { + type: "FPCCPing", + dst: "iframe", + timestamp: Date.now(), + }, + {} as MessageEvent, + ); + } + + sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent): void { + this.fpccProtocol.sendMessage(msg, srcEvent); + } +} diff --git a/dashboard/fp-cloud-connector/page-handler.ts b/dashboard/fp-cloud-connector/page-handler.ts new file mode 100644 index 000000000..4efd20806 --- /dev/null +++ b/dashboard/fp-cloud-connector/page-handler.ts @@ -0,0 +1,85 @@ +/** + * Consumer program that creates and inserts an iframe with in-iframe.ts + */ + +import { CoerceURI, URI } from "@adviser/cement"; +import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; +import { ensureSuperThis } from "@fireproof/core-runtime"; +import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; + +/** + * Creates an iframe element with the specified source + */ +function createIframe(src: string): HTMLIFrameElement { + const iframe = document.createElement("iframe"); + iframe.src = src; + iframe.id = "fireproof-connector-iframe"; + + // Set iframe attributes - make it invisible + iframe.style.display = "none"; + + return iframe; +} + +/** + * Inserts the iframe as the last element in the document body + */ +function insertIframeAsLastElement(iframe: HTMLIFrameElement): void { + // Wait for DOM to be ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + document.body.appendChild(iframe); + }); + } else { + // DOM is already ready + document.body.appendChild(iframe); + } +} + +/** + * Main function to set up the iframe + */ +function initializeIframe( + { + iframeSrc, + }: { + iframeSrc: CoerceURI; + } = { + iframeSrc: "./injected-iframe.html", + }, +): void { + (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { + FP_DEBUG: "*", + }; + let iframeHref: URI; + if (typeof iframeSrc === "string" && iframeSrc.match(/^[./]/)) { + // Infer the path to in-iframe.js from the current module's location + // eslint-disable-next-line no-restricted-globals + const scriptUrl = new URL(import.meta.url); + // eslint-disable-next-line no-restricted-globals + iframeHref = URI.from(new URL(iframeSrc, scriptUrl).href); + } else { + iframeHref = URI.from(iframeSrc); + } + const iframe = createIframe(iframeHref.toString()); + // Add load event listener + const sthis = ensureSuperThis(); + const pageProtocol = new PageFPCCProtocol(sthis); + console.log("Initializing FPCC iframe with src:", iframeHref.toString()); + iframe.addEventListener("load", () => { + window.addEventListener("message", pageProtocol.handleMessage); + pageProtocol.start((event: FPCCMessage) => { + console.log("Sending PageFPCCProtocol", event, iframe.src); + iframe.contentWindow?.postMessage(event, iframe.src); + }); + }); + // Add error event listener + iframe.addEventListener("error", pageProtocol.handleError); + + insertIframeAsLastElement(iframe); +} + +// Initialize when script loads +initializeIframe(); + +export { createIframe, insertIframeAsLastElement, initializeIframe }; diff --git a/dashboard/fp-cloud-connector/post-messager.ts b/dashboard/fp-cloud-connector/post-messager.ts new file mode 100644 index 000000000..731be9bfc --- /dev/null +++ b/dashboard/fp-cloud-connector/post-messager.ts @@ -0,0 +1,135 @@ +/** + * MessageReceiver component for handling postMessage events + */ + +import { Logger } from "@adviser/cement"; +import { ensureLogger } from "@fireproof/core-runtime"; +import { Writable } from "ts-essentials"; +import { SuperThis } from "use-fireproof"; + +export interface MessageEvent { + data: T; + origin: string; + source: WindowProxy | MessagePort | ServiceWorker | null; +} + +export interface PostMessagerConfig { + /** + * Optional origin whitelist for security. If provided, only messages from these origins will be processed. + */ + readonly allowedOrigins?: string[]; + + /** + * Handler function called when a valid message is received + */ + onMessage(data: T, event: MessageEvent): void; + + /** + * Optional error handler + */ + onError?(error: Error, event: MessageEvent): void; + + /** + * Optional validator function to validate message data structure + */ + validator(data: unknown): data is T; + + /** + * Target window to listen on (defaults to current window) + */ + targetWindow?: Window; +} + +export class PostMessager { + private config: Omit, "allowedOrigins"> & Pick>, "allowedOrigins">; + + private listener?: (this: Window, event: MessageEvent) => void; + private targetWindow: Window; + readonly logger: Logger; + readonly id: string; + + constructor(sthis: SuperThis, config: PostMessagerConfig) { + this.config = { allowedOrigins: [], ...config }; + this.targetWindow = config.targetWindow || window; + this.id = sthis.nextId().str; + this.logger = ensureLogger(sthis, "PostMessager").With().Str("Id", this.id).Logger(); + } + + private isMessageEvent(event: MessageEvent): event is MessageEvent { + return this.config.validator && !this.config.validator(event.data); + } + + /** + * Start listening for postMessage events + */ + start(): void { + if (this.listener) { + // eslint-disable-next-line no-console + console.warn("MessageReceiver is already listening"); + return; + } + + this.listener = (event: MessageEvent) => { + try { + // Check origin if whitelist is provided + if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) { + if (!this.config.allowedOrigins.includes(event.origin)) { + // eslint-disable-next-line no-console + console.warn(`Message from unauthorized origin: ${event.origin}`); + return; + } + } + + if (!this.isMessageEvent(event)) { + throw this.logger.Error().Any({ data: event.data }).Msg("Received message with invalid data structure(T)"); + } + + // Call the message handler + this.config.onMessage(event.data, event); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + if (this.config.onError) { + this.config.onError(err, event); + } else { + // eslint-disable-next-line no-console + console.error("Error handling message:", err); + } + } + }; + + this.targetWindow.addEventListener("message", this.listener); + } + + /** + * Stop listening for postMessage events + */ + stop(): void { + if (this.listener) { + this.targetWindow.removeEventListener("message", this.listener); + this.listener = undefined; + } + } + + /** + * Update the allowed origins list + */ + updateAllowedOrigins(origins: string[]): void { + this.config.allowedOrigins = origins; + } + + /** + * Check if the receiver is currently listening + */ + isListening(): boolean { + return this.listener !== null; + } +} + +/** + * Helper function to create and start a message receiver + */ +export function createPostMessager(sthis: SuperThis, config: PostMessagerConfig): PostMessager { + const receiver = new PostMessager(sthis, config); + receiver.start(); + return receiver; +} diff --git a/dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts b/dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts new file mode 100644 index 000000000..86f2978f8 --- /dev/null +++ b/dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts @@ -0,0 +1,151 @@ +import { z } from "zod"; + +// Base message schema (without readonly for extension) +const FPCCMsgBaseSchemaBase = z.object({ + tid: z.string(), + type: z.string(), + src: z.string(), + dst: z.string(), +}); + +export const FPCCMsgBaseSchema = FPCCMsgBaseSchemaBase.readonly(); +export type FPCCMsgBase = z.infer; + +// FPCCEvtNeedsLogin schema +export const FPCCEvtNeedsLoginSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCEvtNeedsLogin"), + devId: z.string(), + loginURL: z.string(), + loginTID: z.string(), + reason: z.enum(["BindCloud", "ConsumeAIToken", "FreeAITokenEnd"]), +}).readonly(); + +export type FPCCEvtNeedsLogin = z.infer; + +// FPCCError schema +export const FPCCErrorSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCError"), + message: z.string(), + cause: z.string().optional(), + stack: z.string().optional(), +}).readonly(); + +export type FPCCError = z.infer; + +// FPCCReqRegisterApp schema +export const FPCCReqRegisterAppSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCReqRegisterApp"), + appURL: z.string(), + appID: z.string(), + localDbNames: z.array(z.string()), +}).readonly(); + +export type FPCCReqRegisterApp = z.infer; + +// FPCCEvtApp schema +export const FPCCEvtAppSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCEvtApp"), + appID: z.string(), + appFavIcon: z + .object({ + defURL: z.string(), + // room for more types and sizes + }) + .readonly(), + devId: z.string(), + user: z + .object({ + name: z.string(), + email: z.string(), + provider: z.enum(["google", "github"]), + iconURL: z.string(), + }) + .readonly(), + localDbs: z.record( + z.string(), + z + .object({ + tenantId: z.string(), + ledgerId: z.string(), + accessToken: z.string(), + }) + .readonly(), + ), + env: z.record(z.string(), z.record(z.string(), z.string())), +}).readonly(); + +export type FPCCEvtApp = z.infer; + +// FPCCPing schema +export const FPCCPingSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCPing"), + timestamp: z.number().optional(), +}).readonly(); + +export type FPCCPing = z.infer; + +// FPCCPong schema +export const FPCCPongSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCPong"), + timestamp: z.number().optional(), + pingTid: z.string(), // Reference to the ping message tid +}).readonly(); + +export type FPCCPong = z.infer; + +// Union schema for all message types +export const FPCCMessageSchema = z.discriminatedUnion("type", [ + FPCCEvtNeedsLoginSchema, + FPCCErrorSchema, + FPCCReqRegisterAppSchema, + FPCCEvtAppSchema, + FPCCPingSchema, + FPCCPongSchema, +]); + +export type FPCCMessage = z.infer; + +export type FPCCSendMessage = Omit, "src"> & { + src?: T["src"] | undefined; + tid?: T["tid"] | undefined; +}; + +// // Send message type - makes src and tid optional for convenience +// export type FPCCSendMessage = { +// [K in keyof FPCCMessage]: K extends 'src' | 'tid' +// ? FPCCMessage[K] | undefined +// : FPCCMessage[K] +// }; + +// Type guard functions + +/** + * Validates if unknown data is a valid FPCC message using Zod safeParse + */ +export function validateFPCCMessage(data: unknown) { + return FPCCMessageSchema.safeParse(data); +} + +export function isFPCCEvtNeedsLogin(msg: FPCCMessage): msg is FPCCEvtNeedsLogin { + return msg.type === "FPCCEvtNeedsLogin"; +} + +export function isFPCCError(msg: FPCCMessage): msg is FPCCError { + return msg.type === "FPCCError"; +} + +export function isFPCCReqRegisterApp(msg: FPCCMessage): msg is FPCCReqRegisterApp { + return msg.type === "FPCCReqRegisterApp"; +} + +export function isFPCCEvtApp(msg: FPCCMessage): msg is FPCCEvtApp { + return msg.type === "FPCCEvtApp"; +} + +export function isFPCCPing(msg: FPCCMessage): msg is FPCCPing { + return msg.type === "FPCCPing"; +} + +export function isFPCCPong(msg: FPCCMessage): msg is FPCCPong { + return msg.type === "FPCCPong"; +} diff --git a/dashboard/package.json b/dashboard/package.json index f438bc5ba..48836acba 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -49,6 +49,7 @@ "react-hook-form": "^7.66.0", "react-router-dom": "^7.9.5", "react-simple-code-editor": "^0.14.1", + "ts-essentials": "^10.1.1", "use-editable": "^2.3.3", "use-fireproof": "workspace:0.0.0", "zod": "^4.1.12" diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 1968c0b8f..75025b747 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig } from "vite"; import { dotenv } from "zx"; import { cloudflare } from "@cloudflare/vite-plugin"; import * as path from "path"; +import * as fs from "fs"; function defines() { try { @@ -29,6 +30,29 @@ export default defineConfig({ react(), cloudflare(), // visualizer(), + { + name: "serve-fp-cloud-connector", + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (req.url?.startsWith("/fp-cloud-connector/")) { + const filePath = path.join(__dirname, req.url.split("?")[0]); + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + const ext = path.extname(filePath); + // Only serve HTML directly, let Vite handle .ts/.js files + if (ext === ".html") { + const content = fs.readFileSync(filePath); + res.setHeader("Content-Type", "text/html"); + res.end(content); + return; + } + // For .ts/.js files, let Vite's normal processing handle them + } + } + next(); + }); + }, + }, +>>>>>>> 4cf9f42f (chore: intro of fp-cloud-connector) ], define: { ...defines(), @@ -43,11 +67,13 @@ export default defineConfig({ server: { port: 7370, hmr: false, - proxy: { - "/*": { - rewrite: () => "/index.html", - }, + fs: { + allow: [ + // Allow serving files from the project root + "..", + ], }, + allowedHosts: ["localhost", "dev-local-1.adviser.com", "dev-local-2.adviser.com"], }, resolve: process.env.USE_SOURCE ? { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23d503c74..3393ea2af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,6 +386,34 @@ importers: specifier: ^8.8.5 version: 8.8.5 + cloud/box-party: + dependencies: + '@adviser/cement': + specifier: ^0.4.63 + version: 0.4.63(typescript@5.9.3) + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + use-fireproof: + specifier: workspace:0.0.0 + version: link:../../use-fireproof + devDependencies: + '@types/react': + specifier: ^19.2.2 + version: 19.2.3 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.3(@types/react@19.2.3) + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + vite: + specifier: ^7.1.12 + version: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + cloud/todo-app: dependencies: '@adviser/cement': @@ -1132,6 +1160,9 @@ importers: react-simple-code-editor: specifier: ^0.14.1 version: 0.14.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + ts-essentials: + specifier: ^10.1.1 + version: 10.1.1(typescript@5.9.3) use-editable: specifier: ^2.3.3 version: 2.3.3(react@19.2.0) From 152d9afde5cad5b09d896157ab65475a3a5d6c78 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 21 Oct 2025 15:58:46 +0200 Subject: [PATCH 02/23] chore: register DB runs agains memory Mock --- cloud/3rd-party/package.json | 2 + cloud/3rd-party/src/App.tsx | 12 +- cloud/3rd-party/src/overlayHtml.tsx | 16 + cloud/3rd-party/vite.config.ts | 4 + core/device-id/device-id-protocol.ts | 19 +- dashboard/fp-cloud-connector-test.html | 15 +- .../iframe-fpcc-protocol.ts | 33 -- .../fp-cloud-connector/injected-iframe.html | 9 - .../page-fpcc-protocol.test.ts | 45 --- .../fp-cloud-connector/page-fpcc-protocol.ts | 45 --- dashboard/package.json | 2 +- dashboard/vite.config.ts | 5 +- pnpm-lock.yaml | 26 +- use-fireproof/fp-cloud-connect-strategy.ts | 139 +++++++++ .../fp-cloud-connector/fp-cloud-connector.ts | 7 +- .../fp-cloud-connector/fpcc-protocol.ts | 52 ++-- .../iframe-fpcc-protocol.ts | 290 ++++++++++++++++++ .../fp-cloud-connector/injected-iframe.html | 17 + .../page-fpcc-protocol.test.ts | 100 ++++++ .../fp-cloud-connector/page-fpcc-protocol.ts | 208 +++++++++++++ .../fp-cloud-connector/page-handler.ts | 47 +-- .../fp-cloud-connector/post-messager.ts | 0 .../protocol-fp-cloud-conn.ts | 80 +++-- use-fireproof/html-defaults.tsx | 59 ++++ use-fireproof/index.ts | 2 + use-fireproof/package.json | 5 +- use-fireproof/redirect-strategy.ts | 57 +--- 27 files changed, 1027 insertions(+), 269 deletions(-) create mode 100644 cloud/3rd-party/src/overlayHtml.tsx delete mode 100644 dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts delete mode 100644 dashboard/fp-cloud-connector/injected-iframe.html delete mode 100644 dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts delete mode 100644 dashboard/fp-cloud-connector/page-fpcc-protocol.ts create mode 100644 use-fireproof/fp-cloud-connect-strategy.ts rename {dashboard => use-fireproof}/fp-cloud-connector/fp-cloud-connector.ts (77%) rename {dashboard => use-fireproof}/fp-cloud-connector/fpcc-protocol.ts (73%) create mode 100644 use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts create mode 100644 use-fireproof/fp-cloud-connector/injected-iframe.html create mode 100644 use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts create mode 100644 use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts rename {dashboard => use-fireproof}/fp-cloud-connector/page-handler.ts (57%) rename {dashboard => use-fireproof}/fp-cloud-connector/post-messager.ts (100%) rename {dashboard => use-fireproof}/fp-cloud-connector/protocol-fp-cloud-conn.ts (65%) create mode 100644 use-fireproof/html-defaults.tsx diff --git a/cloud/3rd-party/package.json b/cloud/3rd-party/package.json index 2b571922c..c1b38e077 100644 --- a/cloud/3rd-party/package.json +++ b/cloud/3rd-party/package.json @@ -40,6 +40,8 @@ }, "dependencies": { "@adviser/cement": "^0.4.63", + "preact": "^10.27.2", + "preact-render-to-string": "^6.6.2", "react": "^19.2.0", "react-dom": "^19.2.0", "use-fireproof": "workspace:0.0.0" diff --git a/cloud/3rd-party/src/App.tsx b/cloud/3rd-party/src/App.tsx index eacd2f80f..57d0cc5ca 100644 --- a/cloud/3rd-party/src/App.tsx +++ b/cloud/3rd-party/src/App.tsx @@ -1,19 +1,15 @@ -import { DocWithId, useFireproof, toCloud, RedirectStrategy } from "use-fireproof"; +import { DocWithId, useFireproof, toCloud, FPCloudConnectStrategy } from "use-fireproof"; import React, { useState, useEffect } from "react"; import "./App.css"; +import { overlayHtml } from "./overlayHtml.js"; // import { URI } from "@adviser/cement"; function App() { const { database, attach } = useFireproof("fireproof-5-party", { attach: toCloud({ - strategy: new RedirectStrategy({ + strategy: new FPCloudConnectStrategy({ // overlayCss: defaultOverlayCss, - overlayHtml: (url: string) => `
-
×
- Fireproof Dashboard
- Sign in to Fireproof Dashboard - Redirect to Fireproof -
`, + overlayHtml, }), // dashboardURI: "http://localhost:7370/fp/cloud/api/token", // tokenApiURI: "http://localhost:7370/api", diff --git a/cloud/3rd-party/src/overlayHtml.tsx b/cloud/3rd-party/src/overlayHtml.tsx new file mode 100644 index 000000000..75e8adb63 --- /dev/null +++ b/cloud/3rd-party/src/overlayHtml.tsx @@ -0,0 +1,16 @@ +import { renderToString } from "preact-render-to-string"; +import { h as React } from "preact"; + +export function overlayHtml(url: string) { + return renderToString( +
+
×
+ Fireproof Dashboard +
+ Sign in to Fireproof Dashboard + + Redirect to Fireproof + +
, + ); +} diff --git a/cloud/3rd-party/vite.config.ts b/cloud/3rd-party/vite.config.ts index 1355a1f23..abdd98332 100644 --- a/cloud/3rd-party/vite.config.ts +++ b/cloud/3rd-party/vite.config.ts @@ -13,6 +13,10 @@ export default defineConfig({ external: [".dev.vars"], }, }, + resolve: { + extensions: [".ts", ".tsx", ".js", ".jsx"], + // This makes Vite try .ts if .js doesn't exist + }, server: { port: 3001, hmr: true, diff --git a/core/device-id/device-id-protocol.ts b/core/device-id/device-id-protocol.ts index 3379e4cd5..8398ea5cd 100644 --- a/core/device-id/device-id-protocol.ts +++ b/core/device-id/device-id-protocol.ts @@ -1,14 +1,14 @@ import { IssueCertificateResult, JWKPrivateSchema, SuperThis } from "@fireproof/core-types-base"; -import { CAActions, DeviceIdCA } from "./device-id-CA.js"; +import { DeviceIdCA } from "./device-id-CA.js"; import { param, Result } from "@adviser/cement"; import { DeviceIdKey } from "./device-id-key.js"; import { base58btc } from "multiformats/bases/base58"; import { DeviceIdVerifyMsg, VerifyWithCertificateResult } from "./device-id-verify-msg.js"; -async function ensureCA(sthis: SuperThis, actions: CAActions): Promise> { +async function ensureCA(sthis: SuperThis, opts: DeviceIdProtocolSrvOpts): Promise> { const rEnv = sthis.env.gets({ - DEVICE_ID_CA_KEY: param.REQUIRED, - DEVICE_ID_CA_COMMON_NAME: param.OPTIONAL, + DEVICE_ID_CA_KEY: opts.env?.DEVICE_ID_CA_KEY ?? param.REQUIRED, + DEVICE_ID_CA_COMMON_NAME: opts.env?.DEVICE_ID_CA_COMMON_NAME ?? param.OPTIONAL, }); if (rEnv.isErr()) { throw rEnv.Err(); @@ -29,7 +29,7 @@ async function ensureCA(sthis: SuperThis, actions: CAActions): Promise> { - const rCa = await ensureCA(sthis, opts.actions); + const rCa = await ensureCA(sthis, opts); if (rCa.isErr()) { return Result.Err(rCa); } diff --git a/dashboard/fp-cloud-connector-test.html b/dashboard/fp-cloud-connector-test.html index 7d6d83d30..e70bd3e38 100644 --- a/dashboard/fp-cloud-connector-test.html +++ b/dashboard/fp-cloud-connector-test.html @@ -8,6 +8,19 @@

Interactive Test Fireproof Cloud Connector

- + + diff --git a/dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts b/dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts deleted file mode 100644 index 786ae01d6..000000000 --- a/dashboard/fp-cloud-connector/iframe-fpcc-protocol.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ensureLogger } from "@fireproof/core-runtime"; -import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; -import { FPCCMessage, FPCCMsgBase, FPCCSendMessage } from "./protocol-fp-cloud-conn.js"; -import { SuperThis } from "@fireproof/core-types-base"; -import { Logger } from "@adviser/cement"; - -export class IframeFPCCProtocol implements FPCCProtocol { - readonly sthis: SuperThis; - readonly logger: Logger; - readonly fpccProtocol: FPCCProtocolBase; - - constructor(sthis: SuperThis) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "IframeFPCCProtocol"); - this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); - } - - readonly handleMessage = (event: MessageEvent): void => { - this.fpccProtocol.handleMessage(event); - }; - - readonly handleError = (_error: unknown): void => { - throw new Error("Method not implemented."); - }; - - start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => void): void { - this.fpccProtocol.start(sendFn); - } - - sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): void { - this.fpccProtocol.sendMessage(message, srcEvent); - } -} diff --git a/dashboard/fp-cloud-connector/injected-iframe.html b/dashboard/fp-cloud-connector/injected-iframe.html deleted file mode 100644 index b0a10ed38..000000000 --- a/dashboard/fp-cloud-connector/injected-iframe.html +++ /dev/null @@ -1,9 +0,0 @@ - - - Fireproof Cloud Connector - - - I'm the Fireproof Cloud Connector - - - diff --git a/dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts b/dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts deleted file mode 100644 index 7ba5af97e..000000000 --- a/dashboard/fp-cloud-connector/page-fpcc-protocol.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; -import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; -import { FPCCMessage, FPCCPing } from "./protocol-fp-cloud-conn.js"; -import { ensureSuperThis } from "@fireproof/core-runtime"; - -describe("FPCC Protocol", () => { - const sthis = ensureSuperThis(); - const pageProtocol = new PageFPCCProtocol(sthis); - const iframeProtocol = new IframeFPCCProtocol(sthis); - - iframeProtocol.start((evt: FPCCMessage) => { - pageProtocol.handleMessage({ data: evt, origin: "page" } as MessageEvent); - }); - - function pageProtocolStart() { - pageProtocol.start((evt: FPCCMessage) => { - iframeProtocol.handleMessage({ data: evt, origin: "iframe" } as MessageEvent); - }); - } - - it("ping-pong", () => { - const pingMessage: FPCCPing = { - tid: "test-ping-1", - type: "FPCCPing", - src: "page", - dst: "iframe", - timestamp: Date.now(), - }; - const fpccFn = vi.fn(); - pageProtocol.onFPCCMessage(fpccFn); - pageProtocolStart(); - pageProtocol.sendMessage(pingMessage, {} as MessageEvent); - expect(fpccFn.mock.calls[fpccFn.mock.calls.length - 1]).toEqual([ - { - dst: "page", - pingTid: "test-ping-1", - src: "iframe", - tid: expect.any(String), - timestamp: expect.any(Number), - type: "FPCCPong", - }, - ]); - }); -}); diff --git a/dashboard/fp-cloud-connector/page-fpcc-protocol.ts b/dashboard/fp-cloud-connector/page-fpcc-protocol.ts deleted file mode 100644 index 988682292..000000000 --- a/dashboard/fp-cloud-connector/page-fpcc-protocol.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ensureLogger } from "@fireproof/core-runtime"; -import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; -import { SuperThis } from "@fireproof/core-types-base"; -import { Logger } from "@adviser/cement"; -import { FPCCMessage, FPCCMsgBase, FPCCPing, FPCCSendMessage } from "./protocol-fp-cloud-conn.js"; - -export class PageFPCCProtocol implements FPCCProtocol { - readonly sthis: SuperThis; - readonly logger: Logger; - readonly fpccProtocol: FPCCProtocolBase; - - constructor(sthis: SuperThis) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "PageFPCCProtocol"); - this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); - } - - readonly handleMessage = (_event: MessageEvent): void => { - this.fpccProtocol.handleMessage(_event); - }; - - onFPCCMessage(callback: (msg: FPCCMessage) => boolean | undefined): void { - this.fpccProtocol.onFPCCMessage(callback); - } - - readonly handleError = (_error: unknown): void => { - throw new Error("Method not implemented."); - }; - - start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => void): void { - this.fpccProtocol.start(sendFn); - this.fpccProtocol.sendMessage( - { - type: "FPCCPing", - dst: "iframe", - timestamp: Date.now(), - }, - {} as MessageEvent, - ); - } - - sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent): void { - this.fpccProtocol.sendMessage(msg, srcEvent); - } -} diff --git a/dashboard/package.json b/dashboard/package.json index 48836acba..e44b71ab3 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -51,7 +51,7 @@ "react-simple-code-editor": "^0.14.1", "ts-essentials": "^10.1.1", "use-editable": "^2.3.3", - "use-fireproof": "workspace:0.0.0", + "use-fireproof": "workspace:*", "zod": "^4.1.12" }, "devDependencies": { diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 75025b747..593041aa2 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -64,9 +64,12 @@ export default defineConfig({ emptyOutDir: true, // also necessary manifest: true, }, + // optimizeDeps: { + // include: ['use-fireproof'] + // }, server: { port: 7370, - hmr: false, + // hmr: false, fs: { allow: [ // Allow serving files from the project root diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3393ea2af..926db8d89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,12 @@ importers: '@adviser/cement': specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) + preact: + specifier: ^10.27.2 + version: 10.27.2 + preact-render-to-string: + specifier: ^6.6.2 + version: 6.6.2(preact@10.27.2) react: specifier: ^19.2.0 version: 19.2.0 @@ -1167,7 +1173,7 @@ importers: specifier: ^2.3.3 version: 2.3.3(react@19.2.0) use-fireproof: - specifier: workspace:0.0.0 + specifier: workspace:* version: link:../use-fireproof zod: specifier: ^4.1.12 @@ -1296,12 +1302,21 @@ importers: jose: specifier: ^6.1.1 version: 6.1.1 + preact: + specifier: ^10.27.2 + version: 10.27.2 + preact-render-to-string: + specifier: ^6.6.2 + version: 6.6.2(preact@10.27.2) react: specifier: '>=18.0.0' version: 19.2.0 ts-essentials: specifier: ^10.1.1 version: 10.1.1(typescript@5.9.3) + zod: + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@fireproof/core-cli': specifier: workspace:0.0.0 @@ -5189,6 +5204,11 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.6.2: + resolution: {integrity: sha512-VJ++Pkzv6+ZOmeN/9Qvx0mRdXqnei1Lo3uu9bGvYHhoMI1VUkDT44hcpGbiokl/kuuYTayYa3yvmYTLZMplfMA==} + peerDependencies: + preact: '>=10 || >= 11.0.0-0' + preact@10.24.2: resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} @@ -9889,6 +9909,10 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@6.6.2(preact@10.27.2): + dependencies: + preact: 10.27.2 + preact@10.24.2: {} preact@10.27.2: {} diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts new file mode 100644 index 000000000..96ce14aca --- /dev/null +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -0,0 +1,139 @@ +import { Lazy, Logger } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core-types-base"; +import { ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; +import { hashObjectSync } from "@fireproof/core-runtime"; +import { RedirectStrategyOpts } from "./redirect-strategy.js"; +import { defaultOverlayCss, defaultOverlayHtml } from "./html-defaults.js"; + +import { initializeIframe } from "./fp-cloud-connector/page-handler.js"; + +export interface FPCloudConnectOpts extends RedirectStrategyOpts { + readonly fpCloudConnectURL: string; +} + +// open(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): void; +// tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; +// waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise; +// stop(): void; + +export class FPCloudConnectStrategy implements TokenStrategie { + resultId?: string; + overlayNode?: HTMLDivElement; + waitState: "started" | "stopped" = "stopped"; + + readonly overlayCss: string; + readonly overlayHtml: (redirectLink: string) => string; + readonly fpCloudConnectURL: string; + + constructor(opts: Partial = {}) { + this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); + this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; + this.fpCloudConnectURL = opts.fpCloudConnectURL ?? "./injected-iframe.html"; + } + readonly hash = Lazy(() => + hashObjectSync({ + overlayCss: this.overlayCss, + overlayHtml: this.overlayHtml("X").toString(), + fpCloudConnectURL: this.fpCloudConnectURL, + }), + ); + + open(sthis: SuperThis, logger: Logger, localDbName: string, _opts: ToCloudOpts) { + initializeIframe({ iframeSrc: this.fpCloudConnectURL }).then((proto) => { + console.log("FPCloudConnectStrategy open isReady", localDbName); + + return proto.registerDatabase(localDbName); + + // const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; + // logger.Debug().Url(redirectCtx.dashboardURI).Msg("open redirect"); + // this.resultId = sthis.nextId().str; + // const url = BuildURI.from(redirectCtx.dashboardURI) + // .setParam("back_url", window.location.href) + // .setParam("result_id", this.resultId) + // .setParam("local_ledger_name", localDbName); + + // if (opts.ledger) { + // url.setParam("ledger", opts.ledger); + // } + // if (opts.tenant) { + // url.setParam("tenant", opts.tenant); + // } + + // let overlayNode = document.body.querySelector("#fpOverlay") as HTMLDivElement; + // if (!overlayNode) { + // const styleNode = document.createElement("style"); + // styleNode.innerHTML = DOMPurify.sanitize(this.overlayCss); + // document.head.appendChild(styleNode); + // overlayNode = document.createElement("div") as HTMLDivElement; + // overlayNode.id = "fpOverlay"; + // overlayNode.className = "fpOverlay"; + // overlayNode.innerHTML = DOMPurify.sanitize(this.overlayHtml(url.toString())); + // document.body.appendChild(overlayNode); + // overlayNode.querySelector(".fpCloseButton")?.addEventListener("click", () => { + // if (overlayNode) { + // if (overlayNode.style.display === "block") { + // overlayNode.style.display = "none"; + // this.stop(); + // } else { + // overlayNode.style.display = "block"; + // } + // } + // }); + // } + // overlayNode.style.display = "block"; + // this.overlayNode = overlayNode; + // const width = 800; + // const height = 600; + // const parentScreenX = window.screenX || window.screenLeft; // Cross-browser compatibility + // const parentScreenY = window.screenY || window.screenTop; // Cross-browser compatibility + + // // Get the parent window's outer dimensions (including chrome) + // const parentOuterWidth = window.outerWidth; + // const parentOuterHeight = window.outerHeight; + + // // Calculate the left position for the new window + // // Midpoint of parent window's width - half of new window's width + // const left = parentScreenX + parentOuterWidth / 2 - width / 2; + + // // Calculate the top position for the new window + // // Midpoint of parent window's height - half of new window's height + // const top = parentScreenY + parentOuterHeight / 2 - height / 2; + + // window.open( + // url.asURL(), + // "Fireproof Login", + // `left=${left},top=${top},width=${width},height=${height},scrollbars=yes,resizable=yes,popup=yes`, + // ); + }); + // window.location.href = url.toString(); + } + + // private currentToken?: TokenAndClaims; + + // waiting?: ReturnType; + + stop() { + console.log("FPCloudConnectStrategy stop called"); + // if (this.waiting) { + // clearTimeout(this.waiting); + // this.waiting = undefined; + // } + // this.waitState = "stopped"; + } + + async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { + console.log("FPCloudConnectStrategy tryToken called", opts); + // if (!this.currentToken) { + // const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; + // this.currentToken = await webCtx.token(); + // // console.log("RedirectStrategy tryToken - ctx", this.currentToken); + // } + // return this.currentToken; + return undefined; + } + + async waitForToken(_sthis: SuperThis, _logger: Logger, deviceId: string, opts: ToCloudOpts): Promise { + console.log("FPCloudConnectStrategy waitForToken called", deviceId, opts); + return undefined; + } +} diff --git a/dashboard/fp-cloud-connector/fp-cloud-connector.ts b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts similarity index 77% rename from dashboard/fp-cloud-connector/fp-cloud-connector.ts rename to use-fireproof/fp-cloud-connector/fp-cloud-connector.ts index 267f8b357..c62390a99 100644 --- a/dashboard/fp-cloud-connector/fp-cloud-connector.ts +++ b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts @@ -3,7 +3,7 @@ import { Lazy } from "@adviser/cement"; import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; -const postMessager = Lazy(() => { +export const postMessager = Lazy(() => { (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { FP_DEBUG: "*", }; @@ -11,9 +11,10 @@ const postMessager = Lazy(() => { const protocol = new IframeFPCCProtocol(sthis); window.addEventListener("message", protocol.handleMessage); protocol.start((event: FPCCMessage, srcEvent: MessageEvent) => { + (event as { src: string }).src = event.src ?? window.location.href; + // console.log("postMessager sending message", event); srcEvent.source?.postMessage(event, { targetOrigin: srcEvent.origin }); + return event; }); return protocol; }); - -postMessager(); diff --git a/dashboard/fp-cloud-connector/fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/fpcc-protocol.ts similarity index 73% rename from dashboard/fp-cloud-connector/fpcc-protocol.ts rename to use-fireproof/fp-cloud-connector/fpcc-protocol.ts index 6c5352803..4339ac324 100644 --- a/dashboard/fp-cloud-connector/fpcc-protocol.ts +++ b/use-fireproof/fp-cloud-connector/fpcc-protocol.ts @@ -1,7 +1,7 @@ -import { SuperThis } from "use-fireproof"; import { FPCCMessage, FPCCMsgBase, FPCCPong, FPCCSendMessage, isFPCCPing, validateFPCCMessage } from "./protocol-fp-cloud-conn.js"; import { Logger } from "@adviser/cement"; import { ensureLogger } from "@fireproof/core-runtime"; +import { SuperThis } from "@fireproof/core-types-base"; export interface FPCCProtocol { // handle must be this bound method @@ -9,12 +9,16 @@ export interface FPCCProtocol { handleFPCCMessage?: (event: FPCCMessage, srcEvent: MessageEvent) => void; sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void; handleError: (error: unknown) => void; - start(send: (evt: FPCCMessage, srcEvent: MessageEvent) => void): void; + start(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void; + stop(): void; } export class FPCCProtocolBase implements FPCCProtocol { protected readonly sthis: SuperThis; protected readonly logger: Logger; + readonly #fpccMessageHandlers: ((msg: FPCCMessage, srcEvent: MessageEvent) => boolean | undefined)[] = []; + readonly onStartFns: (() => void)[] = []; + #sendFn: ((msg: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage) | undefined = undefined; constructor(sthis: SuperThis, logger?: Logger) { this.sthis = sthis; @@ -27,7 +31,7 @@ export class FPCCProtocolBase implements FPCCProtocol { return; } const fpCCmsg = validateFPCCMessage(event.data); - console.log("IframeFPCCProtocol handleMessage called", event.data, fpCCmsg.success); + // console.log("IframeFPCCProtocol handleMessage called", event.data, fpCCmsg.success); if (fpCCmsg.success) { this.handleFPCCMessage(fpCCmsg.data, event); } else { @@ -35,28 +39,25 @@ export class FPCCProtocolBase implements FPCCProtocol { } }; - #fpccMessageHandlers: ((msg: FPCCMessage) => boolean | undefined)[] = []; - onFPCCMessage(callback: (msg: FPCCMessage) => boolean | undefined): void { + onFPCCMessage(callback: (msg: FPCCMessage, srcEvent: MessageEvent) => boolean | undefined): void { this.#fpccMessageHandlers.push(callback); } handleFPCCMessage = (event: FPCCMessage, srcEvent: MessageEvent) => { // allow handlers to process the message first and abort further processing - if (this.#fpccMessageHandlers.map((handler) => handler(event)).some((handled) => handled)) { + if (this.#fpccMessageHandlers.map((handler) => handler(event, srcEvent)).some((handled) => handled)) { return; } this.logger.Debug().Any("event", event).Msg("Handling FPCC message"); switch (true) { case isFPCCPing(event): { - this.sendMessage( - { - type: "FPCCPong", - dst: event.src, - pingTid: event.tid, - timestamp: Date.now(), - }, - srcEvent, - ); + const pong: FPCCSendMessage = { + type: "FPCCPong", + dst: event.src, + pingTid: event.tid, + timestamp: Date.now(), + }; + this.sendMessage(pong, srcEvent); break; } } @@ -66,23 +67,32 @@ export class FPCCProtocolBase implements FPCCProtocol { throw new Error("Method not implemented."); }; - #sendFn?: (msg: FPCCMessage, srcEvent: MessageEvent) => void; - start(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent) => void): void { + onStart(fn: () => void): void { + this.onStartFns.push(fn); + } + + start(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { this.#sendFn = sendFn; } - sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent): void { + stop(): void { + this.#sendFn = undefined; + this.#fpccMessageHandlers.splice(0, this.#fpccMessageHandlers.length); + this.onStartFns.splice(0, this.onStartFns.length); + } + + sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent): T { if (!this.#sendFn) { throw new Error("Protocol not started. Call start() before sending messages."); } - this.#sendFn( + return this.#sendFn( { ...msg, - src: msg.src ?? srcEvent.origin ?? "src-unknown", + src: msg.src, tid: msg.tid ?? this.sthis.nextId().str, } as FPCCMessage, srcEvent, - ); + ) as T; } } diff --git a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts new file mode 100644 index 000000000..9ea96cacc --- /dev/null +++ b/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts @@ -0,0 +1,290 @@ +import { ensureLogger, sleep } from "@fireproof/core-runtime"; +import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; +import { + FPCCEvtApp, + FPCCEvtConnectorReady, + FPCCEvtNeedsLogin, + FPCCMessage, + FPCCMsgBase, + FPCCReqRegisterLocalDbName, + FPCCSendMessage, + isFPCCReqRegisterLocalDbName, + isFPCCReqWaitConnectorReady, +} from "./protocol-fp-cloud-conn.js"; +import { SuperThis } from "@fireproof/core-types-base"; +import { BuildURI, KeyedResolvOnce, Logger, Result } from "@adviser/cement"; + +interface IframeFPCCProtocolOpts { + dashboardURI: string; + waitForTokenURI: string; + backend: BackendFPCC; +} + +export interface DbKey { + readonly appId: string; + readonly dbName: string; +} + +export function dbAppKey(o: DbKey): string { + return o.appId + ":" + o.dbName; +} + +interface BackendFPCC { + getState(): "needs-login" | "waiting" | "ready"; + setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready"; + waitForAuthToken(tid: string, tokenURI: string): Promise; + getFPCCEvtApp(): Promise>; + setFPCCEvtApp(app: FPCCEvtApp): Promise; + listRegisteredDbNames(): Promise; + getTokenForDb(dbInfo: DbKey, authToken: string, src: Partial): Promise; +} + +function getBackendFromRegisterLocalDbName(sthis: SuperThis, req: DbKey, deviceId: string): BackendFPCC { + return MemoryFPCCEvtEntity.fromRegisterLocalDbName(sthis, req, deviceId); +} + +const memoryFPCCEvtEntities = new KeyedResolvOnce(); +class MemoryFPCCEvtEntity implements BackendFPCC { + static fromRegisterLocalDbName(sthis: SuperThis, req: DbKey, deviceId: string): MemoryFPCCEvtEntity { + const key = dbAppKey(req); + return memoryFPCCEvtEntities.get(key).once(() => new MemoryFPCCEvtEntity(sthis, req, deviceId)); + } + readonly dbKey: DbKey; + readonly deviceId: string; + readonly sthis: SuperThis; + state: "needs-login" | "waiting" | "ready" = "needs-login"; + constructor(sthis: SuperThis, dbKey: DbKey, deviceId: string) { + this.dbKey = dbKey; + this.deviceId = deviceId; + this.sthis = sthis; + } + + fpccEvtApp?: FPCCEvtApp; + getFPCCEvtApp(): Promise> { + return Promise.resolve(this.fpccEvtApp ? Result.Ok(this.fpccEvtApp) : Result.Err(new Error("No FPCCEvtApp registered"))); + } + + setFPCCEvtApp(app: FPCCEvtApp): Promise { + this.fpccEvtApp = app; + return Promise.resolve(); + } + getState(): "needs-login" | "waiting" | "ready" { + // For testing purposes, we always return "needs-login" + return this.state; + } + + listRegisteredDbNames(): Promise { + return Promise.all( + memoryFPCCEvtEntities + .values() + .map((key) => { + return key.value; + }) + .filter((v) => v.isOk()) + .map((v) => v.Ok().getFPCCEvtApp()), + ).then((apps) => { + console.log("listRegisteredDbNames-o", apps); + return apps.filter((res) => res.isOk()).map((res) => res.Ok()); + }); + } + + setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready" { + const prev = this.state; + this.state = state; + return prev; + } + + waitForAuthToken(tid: string, tokenURI: string): Promise { + return sleep(100).then(() => `fake-auth-token:${tid}:${tokenURI}`); + } + + async getTokenForDb(dbInfo: DbKey, authToken: string, originEvt: Partial): Promise { + await sleep(50); + return { + ...dbInfo, + tid: originEvt.tid ?? this.sthis.nextId(12).str, + type: "FPCCEvtApp", + src: "fp-cloud-connector", + dst: originEvt.src ?? "iframe", + appFavIcon: { + defURL: "https://example.com/favicon.ico", + }, + devId: this.deviceId, + user: { + name: "Test User", + email: "test@example.com", + provider: "google", + iconURL: "https://example.com/icon.png", + }, + localDb: { + dbName: dbInfo.dbName, + tenantId: "tenant-for-" + dbInfo.appId, + ledgerId: "ledger-for-" + dbInfo.appId, + accessToken: `auth-token-for-${dbInfo.appId}-${dbInfo.dbName}-with-${authToken}`, + }, + env: {}, + }; + } +} + +export class IframeFPCCProtocol implements FPCCProtocol { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly fpccProtocol: FPCCProtocolBase; + readonly dashboardURI: string; + readonly waitForTokenURI: string; + + constructor(sthis: SuperThis, opts: Partial = {}) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "IframeFPCCProtocol"); + this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); + this.dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud"; + this.waitForTokenURI = opts.waitForTokenURI ?? "https://dev.connect.fireproof.direct/api"; + } + + readonly handleMessage = (event: MessageEvent): void => { + this.fpccProtocol.handleMessage(event); + }; + + getDeviceId(): string { + return "we-need-to-implement-device-id"; + } + + async needsLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { + console.log("Handling needsLogin for-1", event, this.dashboardURI); + const loginTID = this.sthis.nextId(16).str; + const url = BuildURI.from(this.dashboardURI) + .setParam("back_url", "close") + .setParam("result_id", loginTID) + .setParam("local_ledger_name", event.dbName); + if (event.ledger) { + url.setParam("ledger", event.ledger); + } + if (event.tenant) { + url.setParam("tenant", event.tenant); + } + console.log("Handling needsLogin for-1.5", event, url.toString()); + const fpccEvtNeedsLogin: FPCCSendMessage = { + tid: event.tid, + type: "FPCCEvtNeedsLogin", + dst: event.src, + devId: this.getDeviceId(), + loginURL: url.toString(), + loginTID, + loadDbNames: [ + ...(await backend.listRegisteredDbNames().then((apps) => + apps.map((app) => ({ + appId: app.appId, + dbName: app.localDb.dbName, + })), + )), + event, + ], + reason: "BindCloud", + }; + console.log("Handling needsLogin for-2", event); + for (const dbInfo of fpccEvtNeedsLogin.loadDbNames) { + const backend = getBackendFromRegisterLocalDbName(this.sthis, dbInfo, this.getDeviceId()); + backend.setState("waiting"); + } + console.log("Handling needsLogin for-3", event); + this.sendMessage(fpccEvtNeedsLogin, srcEvent); + console.log("Handling needsLogin for-4", event); + console.log("Sent FPCCEvtNeedsLogin", loginTID, this.waitForTokenURI); + backend.waitForAuthToken(loginTID, this.waitForTokenURI).then((authToken) => { + console.log("Received auth token after login", authToken); + return Promise.allSettled( + fpccEvtNeedsLogin.loadDbNames.map(async (dbInfo) => backend.getTokenForDb(dbInfo, authToken, event)), + ).then((results) => { + results.forEach((res) => { + if (res.status === "fulfilled") { + const fpccEvtApp = res.value; + backend.setFPCCEvtApp(fpccEvtApp); + this.sendMessage(fpccEvtApp, srcEvent); + backend.setState("ready"); + this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); + } else { + this.logger.Error().Err(res.reason).Msg("Failed to obtain token for DB after login"); + } + }); + }); + }); + } + + runStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { + const bstate = backend.getState(); + switch (true) { + case bstate === "ready" && isFPCCReqRegisterLocalDbName(event): + console.log("Backend is ready, sending FPCCEvtApp"); + return backend + .getFPCCEvtApp() + .then((rFpccEvtApp) => { + if (rFpccEvtApp.isOk()) { + this.sendMessage(rFpccEvtApp.Ok(), srcEvent); + } else { + this.logger.Error().Err(rFpccEvtApp).Msg("Failed to get FPCCEvtApp in ready state"); + } + }) + .then(() => Promise.resolve()); + case bstate === "waiting": + { + console.log("Backend is waiting"); + // this.logger.Info().Str("appID", event.appID).Msg("Backend is waiting"); + throw new Error("Backend is in waiting state; not implemented yet."); + } + break; + case bstate === "needs-login" && isFPCCReqRegisterLocalDbName(event): + console.log("Backend needs login"); + return this.needsLogin(backend, event, srcEvent); + + default: + throw this.logger.Error().Str("state", bstate).Msg("Unknown backend state").AsError(); + } + } + + readonly handleFPCCMessage = (event: FPCCMessage, srcEvent: MessageEvent): boolean | undefined => { + switch (true) { + case isFPCCReqRegisterLocalDbName(event): { + this.logger.Info().Str("appID", event.appId).Msg("Received request to register app"); + const backend = getBackendFromRegisterLocalDbName(this.sthis, event, this.getDeviceId()); + console.log("Running state machine for register local db name", backend.getState()); + this.runStateMachine(backend, event, srcEvent); + break; + } + + case isFPCCReqWaitConnectorReady(event): { + this.logger.Info().Str("appID", event.appID).Msg("Received request to wait for connector ready"); + // Here you would implement logic to handle the wait for connector ready request + const readyEvent: FPCCSendMessage = { + type: "FPCCEvtConnectorReady", + timestamp: Date.now(), + seq: event.seq, + devId: this.getDeviceId(), + dst: event.src, + }; + this.sendMessage(readyEvent, srcEvent); + break; + } + } + return undefined; + }; + + readonly handleError = (_error: unknown): void => { + throw new Error("Method not implemented."); + }; + + stop(): void { + this.fpccProtocol.stop(); + } + + start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + this.fpccProtocol.start(sendFn); + this.fpccProtocol.onFPCCMessage(this.handleFPCCMessage); + } + + sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): void { + // message.src = window.location.href; + // console.log("IframeFPCCProtocol sendMessage called", message); + this.fpccProtocol.sendMessage(message, srcEvent); + } +} diff --git a/use-fireproof/fp-cloud-connector/injected-iframe.html b/use-fireproof/fp-cloud-connector/injected-iframe.html new file mode 100644 index 000000000..704f476c2 --- /dev/null +++ b/use-fireproof/fp-cloud-connector/injected-iframe.html @@ -0,0 +1,17 @@ + + + Fireproof Cloud Connector + + + I'm the Fireproof Cloud Connector + + + diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts new file mode 100644 index 000000000..ffbd6f2a2 --- /dev/null +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it, vi } from "vitest"; +import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; +import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; +import { FPCCMessage, FPCCPing } from "./protocol-fp-cloud-conn.js"; +import { ensureSuperThis } from "@fireproof/core-runtime"; +import { URI } from "@adviser/cement"; +import { Writable } from "ts-essentials"; + +describe("FPCC Protocol", () => { + const sthis = ensureSuperThis(); + const pageProtocol = new PageFPCCProtocol(sthis, { + iframeHref: URI.from("https://example.com/iframe"), + loginWaitTime: 1000, + }); + const iframeProtocol = new IframeFPCCProtocol(sthis); + + iframeProtocol.start((evt: Writable) => { + evt.src = evt.src ?? "iframe"; + pageProtocol.handleMessage({ data: evt, origin: "iframe" } as MessageEvent); + return evt; + }); + + function pageProtocolStart() { + pageProtocol.start((evt: Writable) => { + evt.src = evt.src ?? "page"; + iframeProtocol.handleMessage({ data: evt, origin: "page" } as MessageEvent); + return evt; + }); + } + + it("ping-pong", () => { + const pingMessage: FPCCPing = { + tid: "test-ping-1", + type: "FPCCPing", + src: "page", + dst: "iframe", + timestamp: Date.now(), + }; + const fpccFn = vi.fn(); + pageProtocol.onFPCCMessage(fpccFn); + pageProtocolStart(); + pageProtocol.sendMessage(pingMessage); + expect(fpccFn.mock.calls[fpccFn.mock.calls.length - 1]).toEqual([ + { + dst: "page", + pingTid: "test-ping-1", + src: "iframe", + tid: expect.any(String), + timestamp: expect.any(Number), + type: "FPCCPong", + }, + { + data: { + dst: "page", + pingTid: "test-ping-1", + src: "iframe", + tid: expect.any(String), + timestamp: expect.any(Number), + type: "FPCCPong", + }, + origin: "iframe", + }, + ]); + pageProtocol.stop(); + }); + + it("registerApp", async () => { + pageProtocolStart(); + const fpccEvtApp = await pageProtocol.registerDatabase("wurst", { + tid: "tid-test-app-1", + appId: "test-app-1", + }); + expect(fpccEvtApp).toEqual({ + tid: "tid-test-app-1", + type: "FPCCEvtApp", + src: "fp-cloud-connector", + dst: "page", + devId: "we-need-to-implement-device-id", + appId: "test-app-1", + appFavIcon: { + defURL: "https://example.com/favicon.ico", + }, + env: {}, + localDb: { + accessToken: expect.any(String), + // "auth-token-for-test-app-1-wurst-with-fake-auth-token:zMKseTNm6BhLCJNxy6AtXEe:https://dev.connect.fireproof.direct/api", + ledgerId: "ledger-for-test-app-1", + dbName: "wurst", + tenantId: "tenant-for-test-app-1", + }, + user: { + email: "test@example.com", + iconURL: "https://example.com/icon.png", + name: "Test User", + provider: "google", + }, + }); + pageProtocol.stop(); + }); +}); diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts new file mode 100644 index 000000000..a3bc27b38 --- /dev/null +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts @@ -0,0 +1,208 @@ +import { ensureLogger, sleep } from "@fireproof/core-runtime"; +import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; +import { SuperThis } from "@fireproof/core-types-base"; +import { Future, KeyedResolvOnce, Lazy, Logger, URI } from "@adviser/cement"; +import { + FPCCEvtApp, + FPCCMessage, + FPCCMsgBase, + FPCCReqRegisterLocalDbName, + FPCCReqWaitConnectorReady, + FPCCSendMessage, + isFPCCEvtApp, + isFPCCEvtConnectorReady, + isFPCCEvtNeedsLogin, +} from "./protocol-fp-cloud-conn.js"; + +import { dbAppKey } from "./iframe-fpcc-protocol.js"; + +export interface PageFPCCProtocolOpts { + readonly maxConnectRetries?: number; + readonly iframeHref: URI; + readonly loginWaitTime?: number; +} + +interface WaitForFPCCEvtApp { + readonly register: FPCCReqRegisterLocalDbName; + readonly fpccEvtApp: FPCCEvtApp; +} + +export class PageFPCCProtocol implements FPCCProtocol { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly fpccProtocol: FPCCProtocolBase; + readonly maxConnectRetries: number; + readonly dst: string; + + readonly futureConnected = new Future(); + readonly onFPCCEvtNeedsLoginFns = new Set<(msg: FPCCMessage) => void>(); + readonly registerFPCCEvtApp = new KeyedResolvOnce(); + readonly waitforFPCCEvtAppFutures = new Map>(); + waitForConnection?: ReturnType; + readonly loginWaitTime: number; + + constructor(sthis: SuperThis, iopts: PageFPCCProtocolOpts) { + const opts = { + maxConnectRetries: 20, + loginWaitTime: 30000, + ...iopts, + } as Required; + this.sthis = sthis; + this.dst = opts.iframeHref.toString(); + this.logger = ensureLogger(sthis, "PageFPCCProtocol", { + iFrameHref: this.dst, + }); + this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); + this.maxConnectRetries = opts.maxConnectRetries; + this.loginWaitTime = opts.loginWaitTime; + } + + stop(): void { + this.fpccProtocol.stop(); + this.onFPCCEvtNeedsLoginFns.clear(); + this.waitforFPCCEvtAppFutures.clear(); + this.registerFPCCEvtApp.reset(); + if (this.waitForConnection) { + clearInterval(this.waitForConnection); + this.waitForConnection = undefined; + } + } + + readonly handleMessage = (_event: MessageEvent): void => { + this.fpccProtocol.handleMessage(_event); + }; + + onFPCCMessage(callback: (msg: FPCCMessage) => boolean | undefined): void { + this.fpccProtocol.onFPCCMessage(callback); + } + + getAppId(): string { + return "we-need-to-implement-app-id-this"; + } + + readonly handleError = (_error: unknown): void => { + throw new Error("Method not implemented."); + }; + + readonly onceConnected = Lazy((error?: Error) => { + if (error) { + this.logger.Error().Err(error).Msg("Failed to connect FPCCProtocol"); + this.futureConnected.reject(error); + return; + } + this.futureConnected.resolve(); + }); + + registerDatabase(dbName: string, ireg: Partial = {}): Promise { + const reg = { + ...ireg, + tid: ireg.tid, + type: "FPCCReqRegisterLocalDbName", + appId: ireg.appId ?? this.getAppId(), + appURL: ireg.appURL ?? window.location.href, + dbName, + dst: ireg.dst ?? this.dst, + } satisfies FPCCSendMessage; + const key = dbAppKey(reg); + return this.registerFPCCEvtApp + .get(key) + .once(async () => { + if (this.waitforFPCCEvtAppFutures.has(key)) { + throw this.logger + .Error() + .Any({ + key: dbAppKey(reg), + }) + .Msg("multiple waitforFPCCEvtAppFuture in flight") + .AsError(); + } + const fpccEvtAppFuture = new Future(); + this.waitforFPCCEvtAppFutures.set(key, fpccEvtAppFuture); + this.sendMessage(reg); + return { + register: reg, + fpccEvtApp: await Promise.race([ + fpccEvtAppFuture.asPromise(), + sleep(this.loginWaitTime).then(() => { + throw this.logger + .Error() + .Any({ + key: dbAppKey(reg), + }) + .Msg("timeout waiting for FPCCEvtApp") + .AsError(); + }), + ]), + }; + }) + .then(({ fpccEvtApp }) => fpccEvtApp); + } + + start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + this.fpccProtocol.start(sendFn); + let maxTries = 0; + this.waitForConnection = setInterval(() => { + if (maxTries > this.maxConnectRetries) { + this.logger.Error().Msg("FPCC iframe connection timeout."); + clearInterval(this.waitForConnection); + this.waitForConnection = undefined; + return; + } + if (maxTries && maxTries % ~~(this.maxConnectRetries / 2) === 0) { + this.logger.Warn().Int("tried", maxTries).Msg("Waiting for FPCC iframe connector to be ready..."); + } + this.sendMessage({ + src: window.location.href, + type: "FPCCReqWaitConnectorReady", + dst: "iframe", + seq: maxTries++, + timestamp: Date.now(), + appID: this.getAppId(), + }); + }, 100); + + this.onFPCCMessage((msg: FPCCMessage): boolean | undefined => { + // console.log("PageFPCCProtocol received message", msg); + switch (true) { + case isFPCCEvtNeedsLogin(msg): { + this.logger.Info().Any(msg).Msg("Received needs login event from FPCC iframe"); + this.onFPCCEvtNeedsLoginFns.forEach((cb) => cb(msg)); + break; + } + case isFPCCEvtApp(msg): { + const key = dbAppKey({ + appId: msg.appId, + dbName: msg.localDb.dbName, + }); + const future = this.waitforFPCCEvtAppFutures.get(key); + console.log("PAGE-Received FPCCEvtApp for key", key, msg, future); + if (future) { + future.resolve(msg); + this.waitforFPCCEvtAppFutures.delete(key); + } + break; + } + + case isFPCCEvtConnectorReady(msg): { + clearInterval(this.waitForConnection); + this.waitForConnection = undefined; + this.onceConnected(); + return true; + } + } + return undefined; + }); + } + + onFPCCEvtNeedsLogin(callback: (msg: FPCCMessage) => void): void { + this.onFPCCEvtNeedsLoginFns.add(callback); + } + + sendMessage(msg: FPCCSendMessage, srcEvent = new MessageEvent("sendMessage")): T { + return this.fpccProtocol.sendMessage(msg, srcEvent); + } + + connected(): Promise { + return this.futureConnected.asPromise(); + } +} diff --git a/dashboard/fp-cloud-connector/page-handler.ts b/use-fireproof/fp-cloud-connector/page-handler.ts similarity index 57% rename from dashboard/fp-cloud-connector/page-handler.ts rename to use-fireproof/fp-cloud-connector/page-handler.ts index 4efd20806..320f16023 100644 --- a/dashboard/fp-cloud-connector/page-handler.ts +++ b/use-fireproof/fp-cloud-connector/page-handler.ts @@ -2,7 +2,7 @@ * Consumer program that creates and inserts an iframe with in-iframe.ts */ -import { CoerceURI, URI } from "@adviser/cement"; +import { CoerceURI, KeyedResolvOnce, URI } from "@adviser/cement"; import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; import { ensureSuperThis } from "@fireproof/core-runtime"; import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; @@ -36,10 +36,11 @@ function insertIframeAsLastElement(iframe: HTMLIFrameElement): void { } } +const pageProtocolInstance = new KeyedResolvOnce(); /** * Main function to set up the iframe */ -function initializeIframe( +export function initializeIframe( { iframeSrc, }: { @@ -47,7 +48,7 @@ function initializeIframe( } = { iframeSrc: "./injected-iframe.html", }, -): void { +) { (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { FP_DEBUG: "*", }; @@ -61,25 +62,33 @@ function initializeIframe( } else { iframeHref = URI.from(iframeSrc); } - const iframe = createIframe(iframeHref.toString()); - // Add load event listener - const sthis = ensureSuperThis(); - const pageProtocol = new PageFPCCProtocol(sthis); - console.log("Initializing FPCC iframe with src:", iframeHref.toString()); - iframe.addEventListener("load", () => { - window.addEventListener("message", pageProtocol.handleMessage); - pageProtocol.start((event: FPCCMessage) => { - console.log("Sending PageFPCCProtocol", event, iframe.src); - iframe.contentWindow?.postMessage(event, iframe.src); + + return pageProtocolInstance.get(iframeHref.toString()).once(() => { + const iframe = createIframe(iframeHref.toString()); + // Add load event listener + const sthis = ensureSuperThis(); + const pageProtocol = new PageFPCCProtocol(sthis, { iframeHref }); + // console.log("Initializing FPCC iframe with src:", iframeHref.toString()); + iframe.addEventListener("load", () => { + window.addEventListener("message", pageProtocol.handleMessage); + pageProtocol.start((event: FPCCMessage) => { + // console.log("Sending PageFPCCProtocol", event, iframe.src); + (event as { dst: string }).dst = iframe.src; + (event as { src: string }).src = window.location.href; + iframe.contentWindow?.postMessage(event, iframe.src); + return event; + }); }); - }); - // Add error event listener - iframe.addEventListener("error", pageProtocol.handleError); + // Add error event listener + iframe.addEventListener("error", pageProtocol.handleError); - insertIframeAsLastElement(iframe); + insertIframeAsLastElement(iframe); + + return pageProtocol.connected().then(() => pageProtocol); + }); } // Initialize when script loads -initializeIframe(); +// initializeIframe(); -export { createIframe, insertIframeAsLastElement, initializeIframe }; +// export { createIframe, insertIframeAsLastElement, initializeIframe }; diff --git a/dashboard/fp-cloud-connector/post-messager.ts b/use-fireproof/fp-cloud-connector/post-messager.ts similarity index 100% rename from dashboard/fp-cloud-connector/post-messager.ts rename to use-fireproof/fp-cloud-connector/post-messager.ts diff --git a/dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts b/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts similarity index 65% rename from dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts rename to use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts index 86f2978f8..2a0b1a9e0 100644 --- a/dashboard/fp-cloud-connector/protocol-fp-cloud-conn.ts +++ b/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts @@ -17,6 +17,16 @@ export const FPCCEvtNeedsLoginSchema = FPCCMsgBaseSchemaBase.extend({ devId: z.string(), loginURL: z.string(), loginTID: z.string(), + loadDbNames: z.array( + z + .object({ + appId: z.string(), + dbName: z.string(), + tenantId: z.string().optional(), + ledgerId: z.string().optional(), + }) + .readonly(), + ), reason: z.enum(["BindCloud", "ConsumeAIToken", "FreeAITokenEnd"]), }).readonly(); @@ -32,20 +42,22 @@ export const FPCCErrorSchema = FPCCMsgBaseSchemaBase.extend({ export type FPCCError = z.infer; -// FPCCReqRegisterApp schema -export const FPCCReqRegisterAppSchema = FPCCMsgBaseSchemaBase.extend({ - type: z.literal("FPCCReqRegisterApp"), +// FPCCReqRegisterLocalDbName schema +export const FPCCReqRegisterLocalDbNameSchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCReqRegisterLocalDbName"), appURL: z.string(), - appID: z.string(), - localDbNames: z.array(z.string()), + appId: z.string(), + dbName: z.string(), // localDbName + ledger: z.string().optional(), + tenant: z.string().optional(), }).readonly(); -export type FPCCReqRegisterApp = z.infer; +export type FPCCReqRegisterLocalDbName = z.infer; // FPCCEvtApp schema export const FPCCEvtAppSchema = FPCCMsgBaseSchemaBase.extend({ type: z.literal("FPCCEvtApp"), - appID: z.string(), + appId: z.string(), appFavIcon: z .object({ defURL: z.string(), @@ -61,16 +73,14 @@ export const FPCCEvtAppSchema = FPCCMsgBaseSchemaBase.extend({ iconURL: z.string(), }) .readonly(), - localDbs: z.record( - z.string(), - z - .object({ - tenantId: z.string(), - ledgerId: z.string(), - accessToken: z.string(), - }) - .readonly(), - ), + localDb: z + .object({ + dbName: z.string(), + tenantId: z.string(), + ledgerId: z.string(), + accessToken: z.string(), + }) + .readonly(), env: z.record(z.string(), z.record(z.string(), z.string())), }).readonly(); @@ -93,14 +103,36 @@ export const FPCCPongSchema = FPCCMsgBaseSchemaBase.extend({ export type FPCCPong = z.infer; +// FPCCEvtConnectorReady schema +export const FPCCEvtConnectorReadySchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCEvtConnectorReady"), + timestamp: z.number(), + seq: z.number(), + devId: z.string(), +}).readonly(); + +export type FPCCEvtConnectorReady = z.infer; + +// FPCCReqWaitConnectorReady schema +export const FPCCReqWaitConnectorReadySchema = FPCCMsgBaseSchemaBase.extend({ + type: z.literal("FPCCReqWaitConnectorReady"), + timestamp: z.number(), + appID: z.string(), + seq: z.number(), +}).readonly(); + +export type FPCCReqWaitConnectorReady = z.infer; + // Union schema for all message types export const FPCCMessageSchema = z.discriminatedUnion("type", [ FPCCEvtNeedsLoginSchema, FPCCErrorSchema, - FPCCReqRegisterAppSchema, + FPCCReqRegisterLocalDbNameSchema, FPCCEvtAppSchema, FPCCPingSchema, FPCCPongSchema, + FPCCEvtConnectorReadySchema, + FPCCReqWaitConnectorReadySchema, ]); export type FPCCMessage = z.infer; @@ -134,8 +166,8 @@ export function isFPCCError(msg: FPCCMessage): msg is FPCCError { return msg.type === "FPCCError"; } -export function isFPCCReqRegisterApp(msg: FPCCMessage): msg is FPCCReqRegisterApp { - return msg.type === "FPCCReqRegisterApp"; +export function isFPCCReqRegisterLocalDbName(msg: FPCCMessage): msg is FPCCReqRegisterLocalDbName { + return msg.type === "FPCCReqRegisterLocalDbName"; } export function isFPCCEvtApp(msg: FPCCMessage): msg is FPCCEvtApp { @@ -149,3 +181,11 @@ export function isFPCCPing(msg: FPCCMessage): msg is FPCCPing { export function isFPCCPong(msg: FPCCMessage): msg is FPCCPong { return msg.type === "FPCCPong"; } + +export function isFPCCEvtConnectorReady(msg: FPCCMessage): msg is FPCCEvtConnectorReady { + return msg.type === "FPCCEvtConnectorReady"; +} + +export function isFPCCReqWaitConnectorReady(msg: FPCCMessage): msg is FPCCReqWaitConnectorReady { + return msg.type === "FPCCReqWaitConnectorReady"; +} diff --git a/use-fireproof/html-defaults.tsx b/use-fireproof/html-defaults.tsx new file mode 100644 index 000000000..26c84497c --- /dev/null +++ b/use-fireproof/html-defaults.tsx @@ -0,0 +1,59 @@ +import { renderToString } from "preact-render-to-string"; +import { h as React } from "preact"; + +export function defaultOverlayHtml(redirectLink: string) { + return renderToString( + <> +
+
×
+ Fireproof Dashboard Sign in to Fireproof Dashboard + + Redirect to Fireproof + +
+ , + ); +} + +export function defaultOverlayCss() { + return ` +.fpContainer { + position: relative; /* Needed for absolute positioning of the overlay */ +} + +.fpOverlay { + display: none; /* Initially hidden */ + position: fixed; /* Covers the whole viewport */ + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */ + z-index: 1; /* Ensure it's on top of other content */ +} + +.fpOverlayContent { + position: absolute; + // width: calc(100vw - 50px); + // height: calc(100vh - 50px); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); /* Center the content */ + // transform: translate(0%, 0%); /* Center the content */ + background-color: white; + color: black; + // margin: 10px; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); +} + +.fpCloseButton { + position: absolute; + top: 10px; + right: 15px; + font-size: 20px; + cursor: pointer; +} +`; +} diff --git a/use-fireproof/index.ts b/use-fireproof/index.ts index 2e493ae8d..9522d397f 100644 --- a/use-fireproof/index.ts +++ b/use-fireproof/index.ts @@ -19,6 +19,8 @@ import { defaultWebToCloudOpts, WebCtx } from "./react/use-attach.js"; import { toCloud as toCloudCore } from "@fireproof/core-gateways-cloud"; import { ensureSuperThis } from "@fireproof/core-runtime"; +export { FPCloudConnectStrategy } from "./fp-cloud-connect-strategy.js"; + export type UseFpToCloudParam = Omit, "context">, "events"> & Partial & { readonly strategy?: TokenStrategie; diff --git a/use-fireproof/package.json b/use-fireproof/package.json index aa39a53de..94ebd7631 100644 --- a/use-fireproof/package.json +++ b/use-fireproof/package.json @@ -34,7 +34,10 @@ "@fireproof/vendor": "workspace:0.0.0", "dompurify": "^3.3.0", "jose": "^6.1.1", - "ts-essentials": "^10.1.1" + "preact": "^10.27.2", + "preact-render-to-string": "^6.6.2", + "ts-essentials": "^10.1.1", + "zod": "^4.1.12" }, "peerDependencies": { "@adviser/cement": ">=0.4.20", diff --git a/use-fireproof/redirect-strategy.ts b/use-fireproof/redirect-strategy.ts index d02990e26..376b83b0b 100644 --- a/use-fireproof/redirect-strategy.ts +++ b/use-fireproof/redirect-strategy.ts @@ -7,60 +7,9 @@ import { Api } from "@fireproof/core-protocols-dashboard"; import { WebToCloudCtx } from "./react/types.js"; import { WebCtx } from "./react/use-attach.js"; import { hashObjectSync } from "@fireproof/core-runtime"; +import { defaultOverlayCss, defaultOverlayHtml } from "./html-defaults.js"; -function defaultOverlayHtml(redirectLink: string) { - return ` -
-
×
- Fireproof Dashboard - Sign in to Fireproof Dashboard - Redirect to Fireproof -
- `; -} - -const defaultOverlayCss = ` -.fpContainer { - position: relative; /* Needed for absolute positioning of the overlay */ -} - -.fpOverlay { - display: none; /* Initially hidden */ - position: fixed; /* Covers the whole viewport */ - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */ - z-index: 1; /* Ensure it's on top of other content */ -} - -.fpOverlayContent { - position: absolute; - // width: calc(100vw - 50px); - // height: calc(100vh - 50px); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); /* Center the content */ - // transform: translate(0%, 0%); /* Center the content */ - background-color: white; - color: black; - // margin: 10px; - padding: 20px; - border-radius: 5px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); -} - -.fpCloseButton { - position: absolute; - top: 10px; - right: 15px; - font-size: 20px; - cursor: pointer; -} -`; - -interface RedirectStrategyOpts { +export interface RedirectStrategyOpts { readonly overlayCss: string; readonly overlayHtml?: (redirectLink: string) => string; } @@ -74,7 +23,7 @@ export class RedirectStrategy implements TokenStrategie { readonly overlayHtml: (redirectLink: string) => string; constructor(opts: Partial = {}) { - this.overlayCss = opts.overlayCss ?? defaultOverlayCss; + this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; } readonly hash = Lazy(() => From ae03ddf93919022f98ec53d62faf52f7d1b395ef Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 21 Oct 2025 19:40:27 +0200 Subject: [PATCH 03/23] chore: update to be more async --- to have a proper init order --- use-fireproof/fp-cloud-connect-strategy.ts | 213 +++++++++------ .../fp-cloud-connector/fp-cloud-connector.ts | 2 +- .../fp-cloud-connector/fpcc-protocol.ts | 9 +- .../iframe-fpcc-protocol.ts | 63 ++--- .../fp-cloud-connector/injected-iframe.html | 2 +- .../page-fpcc-protocol.test.ts | 25 +- .../fp-cloud-connector/page-fpcc-protocol.ts | 242 ++++++++++-------- .../fp-cloud-connector/page-handler.ts | 65 ++--- .../fp-cloud-connector/post-messager.ts | 2 +- .../protocol-fp-cloud-conn.ts | 2 +- 10 files changed, 351 insertions(+), 274 deletions(-) diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index 96ce14aca..55159d95a 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,14 +1,20 @@ -import { Lazy, Logger } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Lazy, Logger, ResolveSeq, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { hashObjectSync } from "@fireproof/core-runtime"; +import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; import { defaultOverlayCss, defaultOverlayHtml } from "./html-defaults.js"; import { initializeIframe } from "./fp-cloud-connector/page-handler.js"; +import { PageFPCCProtocol } from "./fp-cloud-connector/page-fpcc-protocol.js"; +import { FPCCEvtNeedsLogin } from "./fp-cloud-connector/protocol-fp-cloud-conn.js"; +import DOMPurify from "dompurify"; +import { dbAppKey } from "./fp-cloud-connector/iframe-fpcc-protocol.js"; export interface FPCloudConnectOpts extends RedirectStrategyOpts { readonly fpCloudConnectURL: string; + readonly title?: string; + readonly sthis?: SuperThis; } // open(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): void; @@ -16,19 +22,45 @@ export interface FPCloudConnectOpts extends RedirectStrategyOpts { // waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise; // stop(): void; +const ppageProtocolInstances = new KeyedResolvOnce(); + +function ppageProtocolKey(iframeSrc: string): string { + if (!iframeSrc) { + iframeSrc = "./injected-iframe.html"; + } + let iframeHref: URI; + if (typeof iframeSrc === "string" && iframeSrc.match(/^[./]/)) { + // Infer the path to in-iframe.js from the current module's location + // eslint-disable-next-line no-restricted-globals + const scriptUrl = new URL(import.meta.url); + // eslint-disable-next-line no-restricted-globals + iframeHref = URI.from(new URL(iframeSrc, scriptUrl).href); + } else { + iframeHref = URI.from(iframeSrc); + } + return iframeHref.toString(); +} + +const registerLocalDbNames = new KeyedResolvOnce, string>(); + export class FPCloudConnectStrategy implements TokenStrategie { - resultId?: string; overlayNode?: HTMLDivElement; waitState: "started" | "stopped" = "stopped"; readonly overlayCss: string; readonly overlayHtml: (redirectLink: string) => string; + readonly title: string; readonly fpCloudConnectURL: string; + readonly sthis: SuperThis; + readonly logger: Logger; constructor(opts: Partial = {}) { this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; this.fpCloudConnectURL = opts.fpCloudConnectURL ?? "./injected-iframe.html"; + this.title = opts.title ?? "Fireproof Login"; + this.sthis = opts.sthis ?? ensureSuperThis(); + this.logger = ensureLogger(this.sthis, "FPCloudConnectStrategy"); } readonly hash = Lazy(() => hashObjectSync({ @@ -38,74 +70,101 @@ export class FPCloudConnectStrategy implements TokenStrategie { }), ); + openFireproofLogin(msg: FPCCEvtNeedsLogin): void { + // const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; + this.logger.Debug().Url(msg.loginURL).Msg("open redirect"); + + let overlayNode = document.body.querySelector("#fpOverlay") as HTMLDivElement; + if (!overlayNode) { + const styleNode = document.createElement("style"); + styleNode.innerHTML = DOMPurify.sanitize(this.overlayCss); + document.head.appendChild(styleNode); + overlayNode = document.createElement("div") as HTMLDivElement; + overlayNode.id = "fpOverlay"; + overlayNode.className = "fpOverlay"; + overlayNode.innerHTML = DOMPurify.sanitize(this.overlayHtml(msg.loginURL)); + document.body.appendChild(overlayNode); + overlayNode.querySelector(".fpCloseButton")?.addEventListener("click", () => { + if (overlayNode) { + if (overlayNode.style.display === "block") { + overlayNode.style.display = "none"; + this.stop(); + } else { + overlayNode.style.display = "block"; + } + } + }); + } + overlayNode.style.display = "block"; + this.overlayNode = overlayNode; + const width = 800; + const height = 600; + const parentScreenX = window.screenX || window.screenLeft; // Cross-browser compatibility + const parentScreenY = window.screenY || window.screenTop; // Cross-browser compatibility + + // Get the parent window's outer dimensions (including chrome) + const parentOuterWidth = window.outerWidth; + const parentOuterHeight = window.outerHeight; + + // Calculate the left position for the new window + // Midpoint of parent window's width - half of new window's width + const left = parentScreenX + parentOuterWidth / 2 - width / 2; + + // Calculate the top position for the new window + // Midpoint of parent window's height - half of new window's height + const top = parentScreenY + parentOuterHeight / 2 - height / 2; + + window.open( + // eslint-disable-next-line no-restricted-globals + new URL(msg.loginURL), + this.title, + `left=${left},top=${top},width=${width},height=${height},scrollbars=yes,resizable=yes,popup=yes`, + ); + // window.location.href = url.toString(); + } + + readonly openloginSeq = new ResolveSeq(); + readonly waitForTokenPerKey = new KeyedResolvOnce>(); + + getPageProtocol(sthis: SuperThis): Promise { + const key = ppageProtocolKey(this.fpCloudConnectURL); + return ppageProtocolInstances.get(key).once(async () => { + const ppage = new PageFPCCProtocol(sthis, { iframeHref: key }); + await ppage.ready(); + ppage.onFPCCEvtNeedsLogin((msg) => { + this.openloginSeq.add(() => { + // test if all dbs are ready + console.log("FPCloudConnectStrategy detected needs login event"); + this.openFireproofLogin(msg); + }); + // logger.Info().Msg("FPCloudConnectStrategy detected needs login event"); + }); + this.waitForTokenPerKey.get(key).once(() => new Future()); + ppage.onFPCCEvtApp((evt) => { + const key = dbAppKey({ appId: evt.appId, dbName: evt.localDb.dbName }); + const future = this.waitForTokenPerKey.get(key)?.value; + if (future) { + future.resolve({ + token: evt.localDb.accessToken, + claims: {} as TokenAndClaims["claims"], + }); + } + }); + return ppage.ready(); + }); + } + open(sthis: SuperThis, logger: Logger, localDbName: string, _opts: ToCloudOpts) { - initializeIframe({ iframeSrc: this.fpCloudConnectURL }).then((proto) => { - console.log("FPCloudConnectStrategy open isReady", localDbName); - - return proto.registerDatabase(localDbName); - - // const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; - // logger.Debug().Url(redirectCtx.dashboardURI).Msg("open redirect"); - // this.resultId = sthis.nextId().str; - // const url = BuildURI.from(redirectCtx.dashboardURI) - // .setParam("back_url", window.location.href) - // .setParam("result_id", this.resultId) - // .setParam("local_ledger_name", localDbName); - - // if (opts.ledger) { - // url.setParam("ledger", opts.ledger); - // } - // if (opts.tenant) { - // url.setParam("tenant", opts.tenant); - // } - - // let overlayNode = document.body.querySelector("#fpOverlay") as HTMLDivElement; - // if (!overlayNode) { - // const styleNode = document.createElement("style"); - // styleNode.innerHTML = DOMPurify.sanitize(this.overlayCss); - // document.head.appendChild(styleNode); - // overlayNode = document.createElement("div") as HTMLDivElement; - // overlayNode.id = "fpOverlay"; - // overlayNode.className = "fpOverlay"; - // overlayNode.innerHTML = DOMPurify.sanitize(this.overlayHtml(url.toString())); - // document.body.appendChild(overlayNode); - // overlayNode.querySelector(".fpCloseButton")?.addEventListener("click", () => { - // if (overlayNode) { - // if (overlayNode.style.display === "block") { - // overlayNode.style.display = "none"; - // this.stop(); - // } else { - // overlayNode.style.display = "block"; - // } - // } - // }); - // } - // overlayNode.style.display = "block"; - // this.overlayNode = overlayNode; - // const width = 800; - // const height = 600; - // const parentScreenX = window.screenX || window.screenLeft; // Cross-browser compatibility - // const parentScreenY = window.screenY || window.screenTop; // Cross-browser compatibility - - // // Get the parent window's outer dimensions (including chrome) - // const parentOuterWidth = window.outerWidth; - // const parentOuterHeight = window.outerHeight; - - // // Calculate the left position for the new window - // // Midpoint of parent window's width - half of new window's width - // const left = parentScreenX + parentOuterWidth / 2 - width / 2; - - // // Calculate the top position for the new window - // // Midpoint of parent window's height - half of new window's height - // const top = parentScreenY + parentOuterHeight / 2 - height / 2; - - // window.open( - // url.asURL(), - // "Fireproof Login", - // `left=${left},top=${top},width=${width},height=${height},scrollbars=yes,resizable=yes,popup=yes`, - // ); + return this.getPageProtocol(sthis).then((ppage) => { + return registerLocalDbNames.get(`${localDbName}:${ppage.getAppId()}:${ppage.dst}`).once(() => { + return initializeIframe(ppage).then((proto) => { + console.log("FPCloudConnectStrategy open isReady", localDbName); + return proto.registerDatabase(localDbName).then((evt) => { + console.log("FPCloudConnectStrategy open registered", evt); + }); + }); + }); }); - // window.location.href = url.toString(); } // private currentToken?: TokenAndClaims; @@ -132,8 +191,18 @@ export class FPCloudConnectStrategy implements TokenStrategie { return undefined; } - async waitForToken(_sthis: SuperThis, _logger: Logger, deviceId: string, opts: ToCloudOpts): Promise { - console.log("FPCloudConnectStrategy waitForToken called", deviceId, opts); - return undefined; + async waitForToken( + _sthis: SuperThis, + _logger: Logger, + localDbName: string, + _opts: ToCloudOpts, + ): Promise { + const ppage = await this.getPageProtocol(this.sthis); + const key = dbAppKey({ appId: ppage.getAppId(), dbName: localDbName }); + const future = this.waitForTokenPerKey.get(key).value; + if (future) { + return future.asPromise(); + } + throw this.logger.Error().Any({ key }).Msg("waitForToken should never be called her"); } } diff --git a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts index c62390a99..d4dafe05a 100644 --- a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts +++ b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts @@ -10,7 +10,7 @@ export const postMessager = Lazy(() => { const sthis = ensureSuperThis(); const protocol = new IframeFPCCProtocol(sthis); window.addEventListener("message", protocol.handleMessage); - protocol.start((event: FPCCMessage, srcEvent: MessageEvent) => { + protocol.injectSend((event: FPCCMessage, srcEvent: MessageEvent) => { (event as { src: string }).src = event.src ?? window.location.href; // console.log("postMessager sending message", event); srcEvent.source?.postMessage(event, { targetOrigin: srcEvent.origin }); diff --git a/use-fireproof/fp-cloud-connector/fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/fpcc-protocol.ts index 4339ac324..d3024aa2c 100644 --- a/use-fireproof/fp-cloud-connector/fpcc-protocol.ts +++ b/use-fireproof/fp-cloud-connector/fpcc-protocol.ts @@ -9,7 +9,8 @@ export interface FPCCProtocol { handleFPCCMessage?: (event: FPCCMessage, srcEvent: MessageEvent) => void; sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void; handleError: (error: unknown) => void; - start(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void; + injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void; + ready(): Promise; stop(): void; } @@ -67,11 +68,11 @@ export class FPCCProtocolBase implements FPCCProtocol { throw new Error("Method not implemented."); }; - onStart(fn: () => void): void { - this.onStartFns.push(fn); + ready(): Promise { + return Promise.resolve(this); } - start(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + injectSend(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { this.#sendFn = sendFn; } diff --git a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts index 9ea96cacc..9866c8d58 100644 --- a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts +++ b/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts @@ -151,7 +151,6 @@ export class IframeFPCCProtocol implements FPCCProtocol { } async needsLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { - console.log("Handling needsLogin for-1", event, this.dashboardURI); const loginTID = this.sthis.nextId(16).str; const url = BuildURI.from(this.dashboardURI) .setParam("back_url", "close") @@ -163,7 +162,6 @@ export class IframeFPCCProtocol implements FPCCProtocol { if (event.tenant) { url.setParam("tenant", event.tenant); } - console.log("Handling needsLogin for-1.5", event, url.toString()); const fpccEvtNeedsLogin: FPCCSendMessage = { tid: event.tid, type: "FPCCEvtNeedsLogin", @@ -182,17 +180,12 @@ export class IframeFPCCProtocol implements FPCCProtocol { ], reason: "BindCloud", }; - console.log("Handling needsLogin for-2", event); for (const dbInfo of fpccEvtNeedsLogin.loadDbNames) { const backend = getBackendFromRegisterLocalDbName(this.sthis, dbInfo, this.getDeviceId()); backend.setState("waiting"); } - console.log("Handling needsLogin for-3", event); this.sendMessage(fpccEvtNeedsLogin, srcEvent); - console.log("Handling needsLogin for-4", event); - console.log("Sent FPCCEvtNeedsLogin", loginTID, this.waitForTokenURI); backend.waitForAuthToken(loginTID, this.waitForTokenURI).then((authToken) => { - console.log("Received auth token after login", authToken); return Promise.allSettled( fpccEvtNeedsLogin.loadDbNames.map(async (dbInfo) => backend.getTokenForDb(dbInfo, authToken, event)), ).then((results) => { @@ -202,7 +195,7 @@ export class IframeFPCCProtocol implements FPCCProtocol { backend.setFPCCEvtApp(fpccEvtApp); this.sendMessage(fpccEvtApp, srcEvent); backend.setState("ready"); - this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); + // this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); } else { this.logger.Error().Err(res.reason).Msg("Failed to obtain token for DB after login"); } @@ -243,28 +236,32 @@ export class IframeFPCCProtocol implements FPCCProtocol { } readonly handleFPCCMessage = (event: FPCCMessage, srcEvent: MessageEvent): boolean | undefined => { - switch (true) { - case isFPCCReqRegisterLocalDbName(event): { - this.logger.Info().Str("appID", event.appId).Msg("Received request to register app"); - const backend = getBackendFromRegisterLocalDbName(this.sthis, event, this.getDeviceId()); - console.log("Running state machine for register local db name", backend.getState()); - this.runStateMachine(backend, event, srcEvent); - break; - } + try { + switch (true) { + case isFPCCReqRegisterLocalDbName(event): { + this.logger.Info().Any(event).Msg("Iframe-Received request to register app"); + const backend = getBackendFromRegisterLocalDbName(this.sthis, event, this.getDeviceId()); + console.log("Running state machine for register local db name", backend.getState()); + this.runStateMachine(backend, event, srcEvent); + break; + } - case isFPCCReqWaitConnectorReady(event): { - this.logger.Info().Str("appID", event.appID).Msg("Received request to wait for connector ready"); - // Here you would implement logic to handle the wait for connector ready request - const readyEvent: FPCCSendMessage = { - type: "FPCCEvtConnectorReady", - timestamp: Date.now(), - seq: event.seq, - devId: this.getDeviceId(), - dst: event.src, - }; - this.sendMessage(readyEvent, srcEvent); - break; + case isFPCCReqWaitConnectorReady(event): { + this.logger.Info().Str("appID", event.appId).Msg("Received request to wait for connector ready"); + // Here you would implement logic to handle the wait for connector ready request + const readyEvent: FPCCSendMessage = { + type: "FPCCEvtConnectorReady", + timestamp: Date.now(), + seq: event.seq, + devId: this.getDeviceId(), + dst: event.src, + }; + this.sendMessage(readyEvent, srcEvent); + break; + } } + } catch (error) { + this.logger.Error().Err(error).Msg("Error handling FPCC message"); } return undefined; }; @@ -274,12 +271,18 @@ export class IframeFPCCProtocol implements FPCCProtocol { }; stop(): void { + console.log("IframeFPCCProtocol stop called"); this.fpccProtocol.stop(); } - start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { - this.fpccProtocol.start(sendFn); + injectSend(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + this.fpccProtocol.injectSend(sendFn); + } + + async ready(): Promise { + await this.fpccProtocol.ready(); this.fpccProtocol.onFPCCMessage(this.handleFPCCMessage); + return this; } sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): void { diff --git a/use-fireproof/fp-cloud-connector/injected-iframe.html b/use-fireproof/fp-cloud-connector/injected-iframe.html index 704f476c2..e38353a1a 100644 --- a/use-fireproof/fp-cloud-connector/injected-iframe.html +++ b/use-fireproof/fp-cloud-connector/injected-iframe.html @@ -11,7 +11,7 @@ } catch (e) { fpcc = await import(new URL("fp-cloud-connector.ts", window.location.href)); } - fpcc.postMessager(); + fpcc.postMessager().then(() => console.log("injected-iframe-ready")); diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts index ffbd6f2a2..28569cf3e 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts @@ -3,32 +3,35 @@ import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; import { FPCCMessage, FPCCPing } from "./protocol-fp-cloud-conn.js"; import { ensureSuperThis } from "@fireproof/core-runtime"; -import { URI } from "@adviser/cement"; import { Writable } from "ts-essentials"; describe("FPCC Protocol", () => { const sthis = ensureSuperThis(); const pageProtocol = new PageFPCCProtocol(sthis, { - iframeHref: URI.from("https://example.com/iframe"), + iframeHref: "https://example.com/iframe", loginWaitTime: 1000, }); const iframeProtocol = new IframeFPCCProtocol(sthis); - iframeProtocol.start((evt: Writable) => { + iframeProtocol.injectSend((evt: Writable) => { evt.src = evt.src ?? "iframe"; + // console.log("IframeFPCCProtocol sending message", evt); pageProtocol.handleMessage({ data: evt, origin: "iframe" } as MessageEvent); return evt; }); - function pageProtocolStart() { - pageProtocol.start((evt: Writable) => { - evt.src = evt.src ?? "page"; - iframeProtocol.handleMessage({ data: evt, origin: "page" } as MessageEvent); - return evt; + function protocolStart() { + return iframeProtocol.ready().then(() => { + pageProtocol.injectSend((evt: Writable) => { + evt.src = evt.src ?? "page"; + iframeProtocol.handleMessage({ data: evt, origin: "page" } as MessageEvent); + return evt; + }); + return pageProtocol.ready(); }); } - it("ping-pong", () => { + it("ping-pong", async () => { const pingMessage: FPCCPing = { tid: "test-ping-1", type: "FPCCPing", @@ -38,7 +41,7 @@ describe("FPCC Protocol", () => { }; const fpccFn = vi.fn(); pageProtocol.onFPCCMessage(fpccFn); - pageProtocolStart(); + await protocolStart(); pageProtocol.sendMessage(pingMessage); expect(fpccFn.mock.calls[fpccFn.mock.calls.length - 1]).toEqual([ { @@ -65,7 +68,7 @@ describe("FPCC Protocol", () => { }); it("registerApp", async () => { - pageProtocolStart(); + await protocolStart(); const fpccEvtApp = await pageProtocol.registerDatabase("wurst", { tid: "tid-test-app-1", appId: "test-app-1", diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts index a3bc27b38..63d9e5db0 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts @@ -1,9 +1,10 @@ import { ensureLogger, sleep } from "@fireproof/core-runtime"; import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; import { SuperThis } from "@fireproof/core-types-base"; -import { Future, KeyedResolvOnce, Lazy, Logger, URI } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Logger, ResolveOnce } from "@adviser/cement"; import { FPCCEvtApp, + FPCCEvtNeedsLogin, FPCCMessage, FPCCMsgBase, FPCCReqRegisterLocalDbName, @@ -18,7 +19,7 @@ import { dbAppKey } from "./iframe-fpcc-protocol.js"; export interface PageFPCCProtocolOpts { readonly maxConnectRetries?: number; - readonly iframeHref: URI; + readonly iframeHref: string; readonly loginWaitTime?: number; } @@ -34,12 +35,14 @@ export class PageFPCCProtocol implements FPCCProtocol { readonly maxConnectRetries: number; readonly dst: string; - readonly futureConnected = new Future(); - readonly onFPCCEvtNeedsLoginFns = new Set<(msg: FPCCMessage) => void>(); + // readonly futureConnected = new Future(); + readonly onFPCCEvtNeedsLoginFns = new Set<(msg: FPCCEvtNeedsLogin) => void>(); + readonly onFPCCEvtAppFns = new Set<(msg: FPCCEvtApp) => void>(); readonly registerFPCCEvtApp = new KeyedResolvOnce(); readonly waitforFPCCEvtAppFutures = new Map>(); waitForConnection?: ReturnType; readonly loginWaitTime: number; + readonly starter = new ResolveOnce(); constructor(sthis: SuperThis, iopts: PageFPCCProtocolOpts) { const opts = { @@ -59,9 +62,11 @@ export class PageFPCCProtocol implements FPCCProtocol { stop(): void { this.fpccProtocol.stop(); + this.onFPCCEvtAppFns.clear(); this.onFPCCEvtNeedsLoginFns.clear(); this.waitforFPCCEvtAppFutures.clear(); this.registerFPCCEvtApp.reset(); + this.starter.reset(); if (this.waitForConnection) { clearInterval(this.waitForConnection); this.waitForConnection = undefined; @@ -77,6 +82,7 @@ export class PageFPCCProtocol implements FPCCProtocol { } getAppId(): string { + // setup in ready return "we-need-to-implement-app-id-this"; } @@ -84,125 +90,139 @@ export class PageFPCCProtocol implements FPCCProtocol { throw new Error("Method not implemented."); }; - readonly onceConnected = Lazy((error?: Error) => { - if (error) { - this.logger.Error().Err(error).Msg("Failed to connect FPCCProtocol"); - this.futureConnected.reject(error); - return; - } - this.futureConnected.resolve(); - }); - - registerDatabase(dbName: string, ireg: Partial = {}): Promise { - const reg = { - ...ireg, - tid: ireg.tid, - type: "FPCCReqRegisterLocalDbName", - appId: ireg.appId ?? this.getAppId(), - appURL: ireg.appURL ?? window.location.href, - dbName, - dst: ireg.dst ?? this.dst, - } satisfies FPCCSendMessage; - const key = dbAppKey(reg); - return this.registerFPCCEvtApp - .get(key) - .once(async () => { - if (this.waitforFPCCEvtAppFutures.has(key)) { - throw this.logger - .Error() - .Any({ - key: dbAppKey(reg), - }) - .Msg("multiple waitforFPCCEvtAppFuture in flight") - .AsError(); - } - const fpccEvtAppFuture = new Future(); - this.waitforFPCCEvtAppFutures.set(key, fpccEvtAppFuture); - this.sendMessage(reg); - return { - register: reg, - fpccEvtApp: await Promise.race([ - fpccEvtAppFuture.asPromise(), - sleep(this.loginWaitTime).then(() => { - throw this.logger - .Error() - .Any({ - key: dbAppKey(reg), - }) - .Msg("timeout waiting for FPCCEvtApp") - .AsError(); - }), - ]), - }; + // readonly onceConnected = Lazy((error?: Error) => { + // if (error) { + // this.logger.Error().Err(error).Msg("Failed to connect FPCCProtocol"); + // this.futureConnected.reject(error); + // return; + // } + // this.futureConnected.resolve(); + // }); + + async registerDatabase(dbName: string, ireg: Partial = {}): Promise { + return this.ready() + .then(() => { + const reg = { + ...ireg, + tid: ireg.tid, + type: "FPCCReqRegisterLocalDbName", + appId: ireg.appId ?? this.getAppId(), + appURL: ireg.appURL ?? window.location.href, + dbName, + dst: ireg.dst ?? this.dst, + } satisfies FPCCSendMessage; + const key = dbAppKey(reg); + return this.registerFPCCEvtApp.get(key).once(async () => { + if (this.waitforFPCCEvtAppFutures.has(key)) { + throw this.logger + .Error() + .Any({ + key: dbAppKey(reg), + }) + .Msg("multiple waitforFPCCEvtAppFuture in flight") + .AsError(); + } + const fpccEvtAppFuture = new Future(); + this.waitforFPCCEvtAppFutures.set(key, fpccEvtAppFuture); + this.sendMessage(reg); + return { + register: reg, + fpccEvtApp: await Promise.race([ + fpccEvtAppFuture.asPromise(), + sleep(this.loginWaitTime).then(() => { + throw this.logger + .Error() + .Any({ + loginWaitTime: this.loginWaitTime, + key: dbAppKey(reg), + }) + .Msg("timeout waiting for FPCCEvtApp") + .AsError(); + }), + ]), + }; + }); }) .then(({ fpccEvtApp }) => fpccEvtApp); } - start(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { - this.fpccProtocol.start(sendFn); - let maxTries = 0; - this.waitForConnection = setInterval(() => { - if (maxTries > this.maxConnectRetries) { - this.logger.Error().Msg("FPCC iframe connection timeout."); - clearInterval(this.waitForConnection); - this.waitForConnection = undefined; - return; - } - if (maxTries && maxTries % ~~(this.maxConnectRetries / 2) === 0) { - this.logger.Warn().Int("tried", maxTries).Msg("Waiting for FPCC iframe connector to be ready..."); - } - this.sendMessage({ - src: window.location.href, - type: "FPCCReqWaitConnectorReady", - dst: "iframe", - seq: maxTries++, - timestamp: Date.now(), - appID: this.getAppId(), - }); - }, 100); - - this.onFPCCMessage((msg: FPCCMessage): boolean | undefined => { - // console.log("PageFPCCProtocol received message", msg); - switch (true) { - case isFPCCEvtNeedsLogin(msg): { - this.logger.Info().Any(msg).Msg("Received needs login event from FPCC iframe"); - this.onFPCCEvtNeedsLoginFns.forEach((cb) => cb(msg)); - break; - } - case isFPCCEvtApp(msg): { - const key = dbAppKey({ - appId: msg.appId, - dbName: msg.localDb.dbName, + injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + this.fpccProtocol.injectSend(send); + } + + ready(): Promise { + return this.starter + .once(async () => { + await this.fpccProtocol.ready(); + let maxTries = 0; + const appId = this.getAppId(); + this.waitForConnection = setInterval(() => { + if (maxTries > this.maxConnectRetries) { + this.logger.Error().Msg("FPCC iframe connection timeout."); + clearInterval(this.waitForConnection); + this.waitForConnection = undefined; + return; + } + if (maxTries && maxTries % ~~(this.maxConnectRetries / 2) === 0) { + this.logger.Warn().Int("tried", maxTries).Msg("Waiting for FPCC iframe connector to be ready..."); + } + this.sendMessage({ + src: window.location.href, + type: "FPCCReqWaitConnectorReady", + dst: "iframe", + seq: maxTries++, + timestamp: Date.now(), + appId, }); - const future = this.waitforFPCCEvtAppFutures.get(key); - console.log("PAGE-Received FPCCEvtApp for key", key, msg, future); - if (future) { - future.resolve(msg); - this.waitforFPCCEvtAppFutures.delete(key); + }, 100); + const waitForConnectorReady = new Future(); + + this.onFPCCMessage((msg: FPCCMessage): boolean | undefined => { + // console.log("PageFPCCProtocol received message", msg); + switch (true) { + case isFPCCEvtNeedsLogin(msg): { + this.logger.Info().Any(msg).Msg("Received needs login event from FPCC iframe"); + this.onFPCCEvtNeedsLoginFns.forEach((cb) => cb(msg)); + break; + } + case isFPCCEvtApp(msg): { + const key = dbAppKey({ + appId: msg.appId, + dbName: msg.localDb.dbName, + }); + const future = this.waitforFPCCEvtAppFutures.get(key); + // console.log("PAGE-Received FPCCEvtApp for key", key, msg, future); + if (future) { + future.resolve(msg); + this.waitforFPCCEvtAppFutures.delete(key); + } + this.onFPCCEvtAppFns.forEach((cb) => cb(msg)); + break; + } + + case isFPCCEvtConnectorReady(msg): { + clearInterval(this.waitForConnection); + this.waitForConnection = undefined; + waitForConnectorReady.resolve(); + return true; + } } - break; - } - - case isFPCCEvtConnectorReady(msg): { - clearInterval(this.waitForConnection); - this.waitForConnection = undefined; - this.onceConnected(); - return true; - } - } - return undefined; - }); + return undefined; + }); + return waitForConnectorReady.asPromise(); + }) + .then(() => this); } - onFPCCEvtNeedsLogin(callback: (msg: FPCCMessage) => void): void { + onFPCCEvtNeedsLogin(callback: (msg: FPCCEvtNeedsLogin) => void): void { this.onFPCCEvtNeedsLoginFns.add(callback); } - sendMessage(msg: FPCCSendMessage, srcEvent = new MessageEvent("sendMessage")): T { - return this.fpccProtocol.sendMessage(msg, srcEvent); + onFPCCEvtApp(callback: (msg: FPCCEvtApp) => void): void { + this.onFPCCEvtAppFns.add(callback); } - connected(): Promise { - return this.futureConnected.asPromise(); + sendMessage(msg: FPCCSendMessage, srcEvent = new MessageEvent("sendMessage")): T { + return this.fpccProtocol.sendMessage(msg, srcEvent); } } diff --git a/use-fireproof/fp-cloud-connector/page-handler.ts b/use-fireproof/fp-cloud-connector/page-handler.ts index 320f16023..e8a03effb 100644 --- a/use-fireproof/fp-cloud-connector/page-handler.ts +++ b/use-fireproof/fp-cloud-connector/page-handler.ts @@ -2,10 +2,10 @@ * Consumer program that creates and inserts an iframe with in-iframe.ts */ -import { CoerceURI, KeyedResolvOnce, URI } from "@adviser/cement"; +import { Future } from "@adviser/cement"; import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; -import { ensureSuperThis } from "@fireproof/core-runtime"; import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; +import { Writable } from "ts-essentials"; /** * Creates an iframe element with the specified source @@ -36,56 +36,37 @@ function insertIframeAsLastElement(iframe: HTMLIFrameElement): void { } } -const pageProtocolInstance = new KeyedResolvOnce(); /** * Main function to set up the iframe */ -export function initializeIframe( - { - iframeSrc, - }: { - iframeSrc: CoerceURI; - } = { - iframeSrc: "./injected-iframe.html", - }, -) { +export function initializeIframe(pageProtocol: PageFPCCProtocol): Promise { (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { FP_DEBUG: "*", }; - let iframeHref: URI; - if (typeof iframeSrc === "string" && iframeSrc.match(/^[./]/)) { - // Infer the path to in-iframe.js from the current module's location - // eslint-disable-next-line no-restricted-globals - const scriptUrl = new URL(import.meta.url); - // eslint-disable-next-line no-restricted-globals - iframeHref = URI.from(new URL(iframeSrc, scriptUrl).href); - } else { - iframeHref = URI.from(iframeSrc); - } - return pageProtocolInstance.get(iframeHref.toString()).once(() => { - const iframe = createIframe(iframeHref.toString()); - // Add load event listener - const sthis = ensureSuperThis(); - const pageProtocol = new PageFPCCProtocol(sthis, { iframeHref }); - // console.log("Initializing FPCC iframe with src:", iframeHref.toString()); - iframe.addEventListener("load", () => { - window.addEventListener("message", pageProtocol.handleMessage); - pageProtocol.start((event: FPCCMessage) => { - // console.log("Sending PageFPCCProtocol", event, iframe.src); - (event as { dst: string }).dst = iframe.src; - (event as { src: string }).src = window.location.href; - iframe.contentWindow?.postMessage(event, iframe.src); - return event; - }); + const iframe = createIframe(pageProtocol.dst); + const waitForLoad = new Future(); + // Add load event listener + // console.log("Initializing FPCC iframe with src:", iframeHref.toString()); + iframe.addEventListener("load", () => { + window.addEventListener("message", pageProtocol.handleMessage); + pageProtocol.injectSend((event: Writable) => { + // console.log("Sending PageFPCCProtocol", event, iframe.src); + event.dst = iframe.src; + event.src = window.location.href; + iframe.contentWindow?.postMessage(event, iframe.src); + return event; + }); + pageProtocol.ready().then(() => { + waitForLoad.resolve(); }); - // Add error event listener - iframe.addEventListener("error", pageProtocol.handleError); + }); + // Add error event listener + iframe.addEventListener("error", pageProtocol.handleError); - insertIframeAsLastElement(iframe); + insertIframeAsLastElement(iframe); - return pageProtocol.connected().then(() => pageProtocol); - }); + return waitForLoad.asPromise().then(() => pageProtocol); } // Initialize when script loads diff --git a/use-fireproof/fp-cloud-connector/post-messager.ts b/use-fireproof/fp-cloud-connector/post-messager.ts index 731be9bfc..c2a9ea6a8 100644 --- a/use-fireproof/fp-cloud-connector/post-messager.ts +++ b/use-fireproof/fp-cloud-connector/post-messager.ts @@ -4,8 +4,8 @@ import { Logger } from "@adviser/cement"; import { ensureLogger } from "@fireproof/core-runtime"; +import { SuperThis } from "@fireproof/core-types-base"; import { Writable } from "ts-essentials"; -import { SuperThis } from "use-fireproof"; export interface MessageEvent { data: T; diff --git a/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts b/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts index 2a0b1a9e0..aae924f6c 100644 --- a/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts +++ b/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts @@ -117,7 +117,7 @@ export type FPCCEvtConnectorReady = z.infer; export const FPCCReqWaitConnectorReadySchema = FPCCMsgBaseSchemaBase.extend({ type: z.literal("FPCCReqWaitConnectorReady"), timestamp: z.number(), - appID: z.string(), + appId: z.string(), seq: z.number(), }).readonly(); From d46d97f822299ab0701a35c80e5aa8435ae9e74f Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 22 Oct 2025 18:04:16 +0200 Subject: [PATCH 04/23] chore: now the real backend has some trouble --- cloud/3rd-party/src/overlayHtml.tsx | 15 +- cloud/3rd-party/tsconfig.json | 5 +- cloud/3rd-party/vite.config.ts | 6 +- core/gateways/base/uri-interceptor.ts | 59 ++++-- core/gateways/cloud/to-cloud.ts | 175 +++++++++--------- .../blockstore/interceptor-gateway.test.ts | 10 +- core/types/protocols/cloud/gateway-control.ts | 8 +- use-fireproof/fp-cloud-connect-strategy.ts | 106 ++++++----- .../fp-cloud-connector/fp-cloud-connector.ts | 3 +- .../fp-cloud-connector/injected-iframe.html | 8 +- .../fp-cloud-connector/page-fpcc-protocol.ts | 87 +++++---- use-fireproof/iframe-strategy.ts | 8 +- use-fireproof/jsx-helper.ts | 6 + ...defaults.tsx => overlay-html-defaults.tsx} | 3 +- use-fireproof/redirect-strategy.ts | 10 +- 15 files changed, 299 insertions(+), 210 deletions(-) create mode 100644 use-fireproof/jsx-helper.ts rename use-fireproof/{html-defaults.tsx => overlay-html-defaults.tsx} (93%) diff --git a/cloud/3rd-party/src/overlayHtml.tsx b/cloud/3rd-party/src/overlayHtml.tsx index 75e8adb63..e7c33fade 100644 --- a/cloud/3rd-party/src/overlayHtml.tsx +++ b/cloud/3rd-party/src/overlayHtml.tsx @@ -1,10 +1,19 @@ import { renderToString } from "preact-render-to-string"; -import { h as React } from "preact"; +import { createElement } from "preact"; +const React = { + createElement, +}; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +// function jsxDEV(...args: unknown[]) { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// return (React as any).call(React, ...args) +// } export function overlayHtml(url: string) { + // return renderToString(h("div", {})) return renderToString( -
-
×
+
+
×
Fireproof Dashboard
Sign in to Fireproof Dashboard diff --git a/cloud/3rd-party/tsconfig.json b/cloud/3rd-party/tsconfig.json index a7e31a70d..9f2759456 100644 --- a/cloud/3rd-party/tsconfig.json +++ b/cloud/3rd-party/tsconfig.json @@ -1,9 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist", - "jsx": "react", - "esModuleInterop": true, - "types": ["react"] + "outDir": "./dist" } } diff --git a/cloud/3rd-party/vite.config.ts b/cloud/3rd-party/vite.config.ts index abdd98332..1e6e56f09 100644 --- a/cloud/3rd-party/vite.config.ts +++ b/cloud/3rd-party/vite.config.ts @@ -2,7 +2,11 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ - plugins: [react()], + plugins: [ + react({ + jsxRuntime: "classic", // Use classic instead of automatic + }), + ], build: { sourcemap: true, target: "esnext", diff --git a/core/gateways/base/uri-interceptor.ts b/core/gateways/base/uri-interceptor.ts index 13c880333..663c8e2f2 100644 --- a/core/gateways/base/uri-interceptor.ts +++ b/core/gateways/base/uri-interceptor.ts @@ -14,7 +14,7 @@ import { SerdeGatewaySubscribeReturn, } from "@fireproof/core-types-blockstore"; -export type URIMapper = (uri: URI) => Promisable; +export type URIMapper = (uri: URI) => Promisable>; export class URIInterceptor extends PassThroughGateway { static withMapper(mapper: URIMapper): URIInterceptor { @@ -28,40 +28,71 @@ export class URIInterceptor extends PassThroughGateway { return this; } - async #map(uri: URI): Promise { - let ret = uri; + async #map(uri: URI): Promise> { + let ret = Result.Ok(uri); for (const mapper of this.#uriMapper) { - ret = await mapper(ret); + ret = await mapper(ret.Ok()); + if (ret.isErr()) { + return ret; + } } return ret; } async buildUrl(ctx: SerdeGatewayCtx, url: URI, key: string): Promise> { - const ret = await super.buildUrl(ctx, await this.#map(url), key); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.buildUrl(ctx, mappedUrl.Ok(), key); return ret; } async start(ctx: SerdeGatewayCtx, url: URI): Promise> { - const ret = await super.start(ctx, await this.#map(url)); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.start(ctx, mappedUrl.Ok()); return ret; } async close(ctx: SerdeGatewayCtx, url: URI): Promise> { - const ret = await super.close(ctx, await this.#map(url)); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.close(ctx, mappedUrl.Ok()); return ret; } async delete(ctx: SerdeGatewayCtx, url: URI): Promise> { - const ret = await super.delete(ctx, await this.#map(url)); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.delete(ctx, mappedUrl.Ok()); return ret; } async destroy(ctx: SerdeGatewayCtx, url: URI): Promise> { - const ret = await super.destroy(ctx, await this.#map(url)); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.destroy(ctx, mappedUrl.Ok()); return ret; } async put(ctx: SerdeGatewayCtx, url: URI, body: FPEnvelope): Promise>> { - const ret = await super.put(ctx, await this.#map(url), body); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.put(ctx, mappedUrl.Ok(), body); return ret; } async get(ctx: SerdeGatewayCtx, url: URI): Promise>> { - const ret = await super.get(ctx, await this.#map(url)); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.get(ctx, mappedUrl.Ok()); return ret; } async subscribe( @@ -69,7 +100,11 @@ export class URIInterceptor extends PassThroughGateway { url: URI, callback: (meta: FPEnvelopeMeta) => Promise, ): Promise> { - const ret = await super.subscribe(ctx, await this.#map(url), callback); + const mappedUrl = await this.#map(url); + if (mappedUrl.isErr()) { + return Result.Err(mappedUrl); + } + const ret = await super.subscribe(ctx, mappedUrl.Ok(), callback); return ret; } } diff --git a/core/gateways/cloud/to-cloud.ts b/core/gateways/cloud/to-cloud.ts index 60adb3db2..8fd51d259 100644 --- a/core/gateways/cloud/to-cloud.ts +++ b/core/gateways/cloud/to-cloud.ts @@ -1,4 +1,4 @@ -import { BuildURI, CoerceURI, Logger, ResolveOnce, URI, AppContext, KeyedResolvOnce, Lazy } from "@adviser/cement"; +import { BuildURI, CoerceURI, URI, AppContext, KeyedResolvOnce, Lazy, Result } from "@adviser/cement"; import { Ledger } from "@fireproof/core-types-base"; import { FPCloudClaim, @@ -70,13 +70,13 @@ export class SimpleTokenStrategy implements TokenStrategie { // console.log("SimpleTokenStrategy open"); return; } - async tryToken(): Promise { - // console.log("SimpleTokenStrategy gatherToken"); - return this.tc; - } - async waitForToken(): Promise { + // async tryToken(): Promise { + // // console.log("SimpleTokenStrategy gatherToken"); + // return this.tc; + // } + async waitForToken(): Promise> { // console.log("SimpleTokenStrategy waitForToken"); - return this.tc; + return Result.Ok(this.tc); } } @@ -121,79 +121,79 @@ function defaultOpts(opts: ToCloudOptionalOpts): ToCloudOpts { // }; // } -function definedExp(exp?: number): number { - if (typeof exp === "number") { - return exp; - } - return new Date().getTime() / 1000; -} - -class TokenObserver { - private readonly opts: ToCloudOpts; - - currentTokenAndClaim?: TokenAndClaims; - - constructor(opts: ToCloudOpts) { - this.opts = opts; - } - - async start() { - return; - } - - async stop() { - // clear pending refresh token - return; - } - - async refreshToken(logger: Logger, ledger: Ledger) { - let token = await this.opts.strategy.tryToken(ledger.sthis, logger, this.opts); - // console.log("refreshToken", token); - if (this.isExpired(token)) { - logger.Debug().Msg("waiting for token"); - this.opts.strategy.open(ledger.sthis, logger, ledger.name, this.opts); - token = await this.opts.strategy.waitForToken(ledger.sthis, logger, ledger.name, this.opts); - if (!token) { - throw new Error("Token not found"); - } - } - return token; - } - - isExpired(token?: TokenAndClaims): boolean { - const now = ~~(new Date().getTime() / 1000); // current time in seconds - return !token || definedExp(token.claims?.exp) - this.opts.refreshTokenPresetSec < now; - } - - readonly _token = new ResolveOnce(); - async getToken(logger: Logger, ledger: Ledger): Promise { - let activeTokenAndClaim = this.currentTokenAndClaim; - if (this.isExpired(activeTokenAndClaim)) { - // console.log("refreshing token", this.currentTokenAndClaim?.claims.exp); - await this.opts.events?.changed(undefined); - logger - .Debug() - .Any({ claims: this.currentTokenAndClaim?.claims, exp: definedExp(this.currentTokenAndClaim?.claims?.exp) }) - .Msg("refresh token"); - activeTokenAndClaim = await this.refreshToken(logger, ledger); - } - - if (activeTokenAndClaim && activeTokenAndClaim.token !== this.currentTokenAndClaim?.token) { - this.currentTokenAndClaim = activeTokenAndClaim; - await this.opts.events?.changed(activeTokenAndClaim); - } - if (this.currentTokenAndClaim) { - return this.currentTokenAndClaim; - } - throw logger.Error().Msg("Token not found").AsError(); - } +// function definedExp(exp?: number): number { +// if (typeof exp === "number") { +// return exp; +// } +// return new Date().getTime() / 1000; +// } - async reset() { - this.currentTokenAndClaim = undefined; - await this.opts.events?.changed(undefined); - return; - } -} +// class TokenObserver { +// private readonly opts: ToCloudOpts; + +// currentTokenAndClaim?: TokenAndClaims; + +// constructor(opts: ToCloudOpts) { +// this.opts = opts; +// } + +// async start() { +// return; +// } + +// async stop() { +// // clear pending refresh token +// return; +// } + +// // async refreshToken(logger: Logger, ledger: Ledger) { +// // let token = await this.opts.strategy.tryToken(ledger.sthis, logger, this.opts); +// // // console.log("refreshToken", token); +// // if (this.isExpired(token)) { +// // logger.Debug().Msg("waiting for token"); +// // this.opts.strategy.open(ledger.sthis, logger, ledger.name, this.opts); +// // token = await this.opts.strategy.waitForToken(ledger.sthis, logger, ledger.name, this.opts); +// // if (!token) { +// // throw new Error("Token not found"); +// // } +// // } +// // return token; +// // } + +// isExpired(token?: TokenAndClaims): boolean { +// const now = ~~(new Date().getTime() / 1000); // current time in seconds +// return !token || definedExp(token.claims?.exp) - this.opts.refreshTokenPresetSec < now; +// } + +// readonly _token = new ResolveOnce(); +// async getToken(logger: Logger, ledger: Ledger): Promise { +// let activeTokenAndClaim = this.currentTokenAndClaim; +// if (this.isExpired(activeTokenAndClaim)) { +// // console.log("refreshing token", this.currentTokenAndClaim?.claims.exp); +// await this.opts.events?.changed(undefined); +// logger +// .Debug() +// .Any({ claims: this.currentTokenAndClaim?.claims, exp: definedExp(this.currentTokenAndClaim?.claims?.exp) }) +// .Msg("refresh token"); +// activeTokenAndClaim = await this.refreshToken(logger, ledger); +// } + +// if (activeTokenAndClaim && activeTokenAndClaim.token !== this.currentTokenAndClaim?.token) { +// this.currentTokenAndClaim = activeTokenAndClaim; +// await this.opts.events?.changed(activeTokenAndClaim); +// } +// if (this.currentTokenAndClaim) { +// return this.currentTokenAndClaim; +// } +// throw logger.Error().Msg("Token not found").AsError(); +// } + +// async reset() { +// this.currentTokenAndClaim = undefined; +// await this.opts.events?.changed(undefined); +// return; +// } +// } class ToCloud implements ToCloudAttachable { readonly opts: ToCloudOpts; @@ -208,7 +208,7 @@ class ToCloud implements ToCloudAttachable { return this.opts.name; } - private _tokenObserver!: TokenObserver; + // private _tokenObserver!: TokenObserver; configHash(db?: Ledger) { const hash = hashObjectSync({ @@ -240,13 +240,18 @@ class ToCloud implements ToCloudAttachable { const logger = ensureLogger(ledger.sthis, "ToCloud"); // .SetDebug("ToCloud"); // console.log("ToCloud prepare", this.opts); - this._tokenObserver = new TokenObserver(this.opts); - await this._tokenObserver.start(); + // this._tokenObserver = new TokenObserver(this.opts); + // await this._tokenObserver.start(); // console.log("prepare"); const gatewayInterceptor = URIInterceptor.withMapper(async (uri) => { // wait for the token - const token = await this._tokenObserver.getToken(logger, ledger); + // const token = await this._tokenObserver.getToken(logger, ledger); + const rToken = await this.opts.strategy.waitForToken(ledger.sthis, logger, ledger.name, this.opts); + if (!rToken.isErr) { + return Result.Err(rToken); + } + const token = rToken.unwrap(); // console.log("getToken", token) const buri = BuildURI.from(uri).setParam("authJWK", token.token); @@ -265,14 +270,14 @@ class ToCloud implements ToCloudAttachable { buri.setParam("ledger", this.opts.ledger); } - return buri.URI(); + return Result.Ok(buri.URI()); }); return { car: { url: this.opts.urls.car, gatewayInterceptor }, file: { url: this.opts.urls.file, gatewayInterceptor }, meta: { url: this.opts.urls.meta, gatewayInterceptor }, teardown: () => { - this._tokenObserver.stop(); + // this._tokenObserver.stop(); }, ctx: this.opts.context, }; diff --git a/core/tests/blockstore/interceptor-gateway.test.ts b/core/tests/blockstore/interceptor-gateway.test.ts index c8ab27e8e..c307345d0 100644 --- a/core/tests/blockstore/interceptor-gateway.test.ts +++ b/core/tests/blockstore/interceptor-gateway.test.ts @@ -226,10 +226,12 @@ describe("InterceptorGateway", () => { base: "uriTest://inspector-gateway", }, gatewayInterceptor: URIInterceptor.withMapper(async (uri: URI) => - uri - .build() - .setParam("itis", "" + ++callCount) - .URI(), + Result.Ok( + uri + .build() + .setParam("itis", "" + ++callCount) + .URI(), + ), ), }); await Promise.all( diff --git a/core/types/protocols/cloud/gateway-control.ts b/core/types/protocols/cloud/gateway-control.ts index bc72880a7..ffaea87e4 100644 --- a/core/types/protocols/cloud/gateway-control.ts +++ b/core/types/protocols/cloud/gateway-control.ts @@ -1,4 +1,4 @@ -import { Logger, CoerceURI, URI, AppContext } from "@adviser/cement"; +import { Logger, CoerceURI, URI, AppContext, Result } from "@adviser/cement"; import { Attachable, SuperThis } from "@fireproof/core-types-base"; import { FPCloudClaim } from "./msg-types.zod.js"; @@ -18,9 +18,9 @@ export interface TokenAndClaims { export interface TokenStrategie { hash(): string; - open(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): void; - tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; - waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise; + open(sthis: SuperThis, logger: Logger, localDbName: string, opts: ToCloudOpts): void; + // tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; + waitForToken(sthis: SuperThis, logger: Logger, localDbName: string, opts: ToCloudOpts): Promise>; stop(): void; } diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index 55159d95a..f50e7a093 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,13 +1,13 @@ -import { Future, KeyedResolvOnce, Lazy, Logger, ResolveSeq, URI } from "@adviser/cement"; +import { KeyedResolvOnce, Lazy, Logger, ResolveSeq, Result, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; +import { ensureLogger, ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; -import { defaultOverlayCss, defaultOverlayHtml } from "./html-defaults.js"; +import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; import { initializeIframe } from "./fp-cloud-connector/page-handler.js"; import { PageFPCCProtocol } from "./fp-cloud-connector/page-fpcc-protocol.js"; -import { FPCCEvtNeedsLogin } from "./fp-cloud-connector/protocol-fp-cloud-conn.js"; +import { FPCCEvtApp, FPCCEvtNeedsLogin } from "./fp-cloud-connector/protocol-fp-cloud-conn.js"; import DOMPurify from "dompurify"; import { dbAppKey } from "./fp-cloud-connector/iframe-fpcc-protocol.js"; @@ -25,9 +25,6 @@ export interface FPCloudConnectOpts extends RedirectStrategyOpts { const ppageProtocolInstances = new KeyedResolvOnce(); function ppageProtocolKey(iframeSrc: string): string { - if (!iframeSrc) { - iframeSrc = "./injected-iframe.html"; - } let iframeHref: URI; if (typeof iframeSrc === "string" && iframeSrc.match(/^[./]/)) { // Infer the path to in-iframe.js from the current module's location @@ -57,7 +54,10 @@ export class FPCloudConnectStrategy implements TokenStrategie { constructor(opts: Partial = {}) { this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; - this.fpCloudConnectURL = opts.fpCloudConnectURL ?? "./injected-iframe.html"; + this.fpCloudConnectURL = + opts.fpCloudConnectURL ?? + // eslint-disable-next-line no-restricted-globals + new URL("fp-cloud-connector/injected-iframe.html", import.meta.url).toString(); this.title = opts.title ?? "Fireproof Login"; this.sthis = opts.sthis ?? ensureSuperThis(); this.logger = ensureLogger(this.sthis, "FPCloudConnectStrategy"); @@ -82,7 +82,9 @@ export class FPCloudConnectStrategy implements TokenStrategie { overlayNode = document.createElement("div") as HTMLDivElement; overlayNode.id = "fpOverlay"; overlayNode.className = "fpOverlay"; - overlayNode.innerHTML = DOMPurify.sanitize(this.overlayHtml(msg.loginURL)); + const myHtml = this.overlayHtml(msg.loginURL); + console.log("FPCloudConnectStrategy openFireproofLogin creating overlay with html", myHtml); + overlayNode.innerHTML = DOMPurify.sanitize(myHtml); document.body.appendChild(overlayNode); overlayNode.querySelector(".fpCloseButton")?.addEventListener("click", () => { if (overlayNode) { @@ -124,45 +126,51 @@ export class FPCloudConnectStrategy implements TokenStrategie { } readonly openloginSeq = new ResolveSeq(); - readonly waitForTokenPerKey = new KeyedResolvOnce>(); + // readonly waitForTokenPerLocalDbFuture = new KeyedResolvOnce>>(); + + fpccEvtApp2TokenAndClaims(evt: FPCCEvtApp): Result { + const tAndC: TokenAndClaims = { + token: evt.localDb.accessToken, + claims: {} as TokenAndClaims["claims"], + }; + return Result.Ok(tAndC); + } getPageProtocol(sthis: SuperThis): Promise { const key = ppageProtocolKey(this.fpCloudConnectURL); return ppageProtocolInstances.get(key).once(async () => { + console.log("FPCloudConnectStrategy creating new PageFPCCProtocol for key", key, import.meta.url); const ppage = new PageFPCCProtocol(sthis, { iframeHref: key }); + await initializeIframe(ppage); await ppage.ready(); ppage.onFPCCEvtNeedsLogin((msg) => { this.openloginSeq.add(() => { // test if all dbs are ready console.log("FPCloudConnectStrategy detected needs login event"); this.openFireproofLogin(msg); + return sleep(10000); }); // logger.Info().Msg("FPCloudConnectStrategy detected needs login event"); }); - this.waitForTokenPerKey.get(key).once(() => new Future()); + // this.waitForTokenPerLocalDbFuture.get(key).once(() => new Future()); ppage.onFPCCEvtApp((evt) => { const key = dbAppKey({ appId: evt.appId, dbName: evt.localDb.dbName }); - const future = this.waitForTokenPerKey.get(key)?.value; - if (future) { - future.resolve({ - token: evt.localDb.accessToken, - claims: {} as TokenAndClaims["claims"], - }); - } + const rTAndC = this.fpccEvtApp2TokenAndClaims(evt); + console.log("FPCloudConnectStrategy received FPCCEvtApp, resolving waitForTokenAndClaims for key", key, rTAndC.Ok()); + this.waitForTokenAndClaims.get(key).reset(() => rTAndC); + // if (future) { + // future.resolve(rTAndC) + // } }); return ppage.ready(); }); } open(sthis: SuperThis, logger: Logger, localDbName: string, _opts: ToCloudOpts) { + console.log("FPCloudConnectStrategy open called for localDbName", localDbName); return this.getPageProtocol(sthis).then((ppage) => { return registerLocalDbNames.get(`${localDbName}:${ppage.getAppId()}:${ppage.dst}`).once(() => { - return initializeIframe(ppage).then((proto) => { - console.log("FPCloudConnectStrategy open isReady", localDbName); - return proto.registerDatabase(localDbName).then((evt) => { - console.log("FPCloudConnectStrategy open registered", evt); - }); - }); + console.log("FPCloudConnectStrategy open registering localDbName", localDbName); }); }); } @@ -180,29 +188,35 @@ export class FPCloudConnectStrategy implements TokenStrategie { // this.waitState = "stopped"; } - async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { - console.log("FPCloudConnectStrategy tryToken called", opts); - // if (!this.currentToken) { - // const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; - // this.currentToken = await webCtx.token(); - // // console.log("RedirectStrategy tryToken - ctx", this.currentToken); - // } - // return this.currentToken; - return undefined; - } - - async waitForToken( - _sthis: SuperThis, - _logger: Logger, - localDbName: string, - _opts: ToCloudOpts, - ): Promise { + readonly waitForTokenAndClaims = new KeyedResolvOnce>(); + // async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { + // console.log("FPCloudConnectStrategy tryToken called", opts); + // // if (!this.currentToken) { + // // const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; + // // this.currentToken = await webCtx.token(); + // // // console.log("RedirectStrategy tryToken - ctx", this.currentToken); + // // } + // // return this.currentToken; + // return undefined; + // } + + async waitForToken(_sthis: SuperThis, _logger: Logger, localDbName: string, _opts: ToCloudOpts): Promise> { + // console.log("FPCloudConnectStrategy waitForToken called for localDbName", localDbName); const ppage = await this.getPageProtocol(this.sthis); const key = dbAppKey({ appId: ppage.getAppId(), dbName: localDbName }); - const future = this.waitForTokenPerKey.get(key).value; - if (future) { - return future.asPromise(); - } - throw this.logger.Error().Any({ key }).Msg("waitForToken should never be called her"); + await this.openloginSeq.flush(); + return this.waitForTokenAndClaims.get(key).once(() => { + return ppage.registerDatabase(localDbName).then((evt) => { + if (evt.isErr()) { + console.log("FPCloudConnectStrategy waitForToken registering database failed for key", key, evt); + return Result.Err(evt); + } + console.log("FPCloudConnectStrategy waitForToken resolving for key", key); + return this.fpccEvtApp2TokenAndClaims(evt.Ok()); + }); + + // const future = this.waitForTokenPerLocalDbFuture.get(key).once(() => new Future>()) + // return future.asPromise(); + }); } } diff --git a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts index d4dafe05a..daaab84d1 100644 --- a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts +++ b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts @@ -3,7 +3,7 @@ import { Lazy } from "@adviser/cement"; import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; -export const postMessager = Lazy(() => { +export const postMessager = Lazy(async () => { (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { FP_DEBUG: "*", }; @@ -16,5 +16,6 @@ export const postMessager = Lazy(() => { srcEvent.source?.postMessage(event, { targetOrigin: srcEvent.origin }); return event; }); + await protocol.ready(); return protocol; }); diff --git a/use-fireproof/fp-cloud-connector/injected-iframe.html b/use-fireproof/fp-cloud-connector/injected-iframe.html index e38353a1a..c674ecb6d 100644 --- a/use-fireproof/fp-cloud-connector/injected-iframe.html +++ b/use-fireproof/fp-cloud-connector/injected-iframe.html @@ -7,9 +7,13 @@ diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts index 63d9e5db0..fc4cfd3df 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts @@ -1,7 +1,7 @@ import { ensureLogger, sleep } from "@fireproof/core-runtime"; import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; import { SuperThis } from "@fireproof/core-types-base"; -import { Future, KeyedResolvOnce, Logger, ResolveOnce } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Logger, ResolveOnce, Result } from "@adviser/cement"; import { FPCCEvtApp, FPCCEvtNeedsLogin, @@ -99,51 +99,66 @@ export class PageFPCCProtocol implements FPCCProtocol { // this.futureConnected.resolve(); // }); - async registerDatabase(dbName: string, ireg: Partial = {}): Promise { - return this.ready() - .then(() => { - const reg = { - ...ireg, - tid: ireg.tid, - type: "FPCCReqRegisterLocalDbName", - appId: ireg.appId ?? this.getAppId(), - appURL: ireg.appURL ?? window.location.href, - dbName, - dst: ireg.dst ?? this.dst, - } satisfies FPCCSendMessage; - const key = dbAppKey(reg); - return this.registerFPCCEvtApp.get(key).once(async () => { + async registerDatabase(dbName: string, ireg: Partial = {}): Promise> { + return this.ready().then(() => { + const sreg = { + ...ireg, + tid: ireg.tid, + type: "FPCCReqRegisterLocalDbName", + appId: ireg.appId ?? this.getAppId(), + appURL: ireg.appURL ?? window.location.href, + dbName, + dst: ireg.dst ?? this.dst, + } satisfies FPCCSendMessage; + const key = dbAppKey(sreg); + return this.registerFPCCEvtApp + .get(key) + .once(async () => { if (this.waitforFPCCEvtAppFutures.has(key)) { - throw this.logger + return this.logger .Error() .Any({ - key: dbAppKey(reg), + key: dbAppKey(sreg), }) .Msg("multiple waitforFPCCEvtAppFuture in flight") - .AsError(); + .ResultError(); } const fpccEvtAppFuture = new Future(); this.waitforFPCCEvtAppFutures.set(key, fpccEvtAppFuture); - this.sendMessage(reg); - return { + const reg = this.sendMessage(sreg); + + const rFPCCEvtApp = await Promise.race([ + fpccEvtAppFuture + .asPromise() + .then((evt) => Result.Ok(evt)) + .catch((error) => Result.Err(error)), + sleep(this.loginWaitTime).then(() => + this.logger + .Error() + .Any({ + loginWaitTime: this.loginWaitTime, + key: dbAppKey(reg), + }) + .Msg("timeout waiting for FPCCEvtApp") + .ResultError(), + ), + ]); + this.waitforFPCCEvtAppFutures.delete(key); + if (rFPCCEvtApp.isErr()) { + throw Result.Err(rFPCCEvtApp); + } + return Result.Ok({ register: reg, - fpccEvtApp: await Promise.race([ - fpccEvtAppFuture.asPromise(), - sleep(this.loginWaitTime).then(() => { - throw this.logger - .Error() - .Any({ - loginWaitTime: this.loginWaitTime, - key: dbAppKey(reg), - }) - .Msg("timeout waiting for FPCCEvtApp") - .AsError(); - }), - ]), - }; + fpccEvtApp: rFPCCEvtApp.unwrap(), + } satisfies WaitForFPCCEvtApp); + }) + .then((rWaitForFPCCEvtApp) => { + if (rWaitForFPCCEvtApp.isErr()) { + return Result.Err(rWaitForFPCCEvtApp); + } + return Result.Ok(rWaitForFPCCEvtApp.unwrap().fpccEvtApp); }); - }) - .then(({ fpccEvtApp }) => fpccEvtApp); + }); } injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { diff --git a/use-fireproof/iframe-strategy.ts b/use-fireproof/iframe-strategy.ts index cdf7e4d51..2c75dda8e 100644 --- a/use-fireproof/iframe-strategy.ts +++ b/use-fireproof/iframe-strategy.ts @@ -1,4 +1,4 @@ -import { BuildURI, Logger } from "@adviser/cement"; +import { BuildURI, Logger, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { TokenStrategie, ToCloudOpts, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; import { WebCtx } from "./react/use-attach.js"; @@ -95,10 +95,8 @@ export class IframeStrategy implements TokenStrategie { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string): Promise { + async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string): Promise> { // throw new Error("waitForToken not implemented"); - return new Promise(() => { - /* */ - }); + return Result.Err("not implemented"); } } diff --git a/use-fireproof/jsx-helper.ts b/use-fireproof/jsx-helper.ts new file mode 100644 index 000000000..183068405 --- /dev/null +++ b/use-fireproof/jsx-helper.ts @@ -0,0 +1,6 @@ +export { renderToString } from "preact-render-to-string"; +import { createElement } from "preact"; + +export const React = { + createElement, +}; diff --git a/use-fireproof/html-defaults.tsx b/use-fireproof/overlay-html-defaults.tsx similarity index 93% rename from use-fireproof/html-defaults.tsx rename to use-fireproof/overlay-html-defaults.tsx index 26c84497c..cf109aa22 100644 --- a/use-fireproof/html-defaults.tsx +++ b/use-fireproof/overlay-html-defaults.tsx @@ -1,5 +1,4 @@ -import { renderToString } from "preact-render-to-string"; -import { h as React } from "preact"; +import { React, renderToString } from "./jsx-helper.js"; export function defaultOverlayHtml(redirectLink: string) { return renderToString( diff --git a/use-fireproof/redirect-strategy.ts b/use-fireproof/redirect-strategy.ts index 376b83b0b..ba07c2e87 100644 --- a/use-fireproof/redirect-strategy.ts +++ b/use-fireproof/redirect-strategy.ts @@ -1,4 +1,4 @@ -import { BuildURI, Lazy, Logger } from "@adviser/cement"; +import { BuildURI, Lazy, Logger, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { decodeJwt } from "jose"; import DOMPurify from "dompurify"; @@ -7,7 +7,7 @@ import { Api } from "@fireproof/core-protocols-dashboard"; import { WebToCloudCtx } from "./react/types.js"; import { WebCtx } from "./react/use-attach.js"; import { hashObjectSync } from "@fireproof/core-runtime"; -import { defaultOverlayCss, defaultOverlayHtml } from "./html-defaults.js"; +import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; export interface RedirectStrategyOpts { readonly overlayCss: string; @@ -152,17 +152,17 @@ export class RedirectStrategy implements TokenStrategie { this.waiting = setTimeout(() => this.getTokenAndClaimsByResultId(logger, dashApi, resultId, opts, resolve), opts.intervalSec); } - async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise { + async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise> { if (!this.resultId) { throw new Error("waitForToken not working on redirect strategy"); } const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; const dashApi = new Api(webCtx.tokenApiURI); this.waitState = "started"; - return new Promise((resolve) => { + return new Promise>((resolve) => { this.getTokenAndClaimsByResultId(logger, dashApi, this.resultId, opts, (tokenAndClaims) => { this.currentToken = tokenAndClaims; - resolve(tokenAndClaims); + resolve(Result.Ok(tokenAndClaims)); }); }); } From 19197e7eecb120f61b28de75f5ef9d544be7c16d Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 22 Oct 2025 18:08:23 +0200 Subject: [PATCH 05/23] chore: protocol test --- use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts index 28569cf3e..7a2d8332e 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts @@ -73,7 +73,7 @@ describe("FPCC Protocol", () => { tid: "tid-test-app-1", appId: "test-app-1", }); - expect(fpccEvtApp).toEqual({ + expect(fpccEvtApp.Ok()).toEqual({ tid: "tid-test-app-1", type: "FPCCEvtApp", src: "fp-cloud-connector", From d886c108c6da3d78e8187741c49ddfaf1cc33643 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Thu, 23 Oct 2025 10:37:23 +0200 Subject: [PATCH 06/23] chore: fix dashboard error --- cli/tsc-cmd.ts | 12 ++++---- core/device-id/device-id-protocol.ts | 11 +++++-- core/gateways/file-node/node-filesystem.ts | 35 ---------------------- dashboard/backend/api.ts | 2 +- dashboard/backend/create-fp-token.ts | 2 +- dashboard/backend/create-handler.ts | 2 +- dashboard/backend/db-api.test.ts | 2 +- dashboard/backend/invites.ts | 2 +- dashboard/src/app-context.tsx | 2 +- dashboard/src/components/DynamicTable.tsx | 2 +- dashboard/src/pages/databases.tsx | 2 +- dashboard/src/pages/databases/connect.tsx | 2 +- dashboard/src/pages/databases/new.tsx | 2 +- dashboard/src/pages/databases/query.tsx | 2 +- 14 files changed, 27 insertions(+), 53 deletions(-) diff --git a/cli/tsc-cmd.ts b/cli/tsc-cmd.ts index 38a93f25e..f9fcac69c 100644 --- a/cli/tsc-cmd.ts +++ b/cli/tsc-cmd.ts @@ -28,11 +28,13 @@ export async function handleTsc(args: string[], sthis: SuperThis) { $.quote = quotePowerShell; } - $.verbose = false; - const p = $({ stdio: ["inherit", "inherit", "inherit"] })`${cmd}`; - await p; - // $.verbose = true; - // await $`${cmd}` + try { + $.verbose = false; + const p = $({ stdio: ["inherit", "inherit", "inherit"] })`${cmd}`; + await p; + } catch (e) { + process.exit((e as { exitCode?: number }).exitCode ?? 42); + } } export function tscCmd(sthis: SuperThis) { diff --git a/core/device-id/device-id-protocol.ts b/core/device-id/device-id-protocol.ts index 8398ea5cd..7ee7edb8b 100644 --- a/core/device-id/device-id-protocol.ts +++ b/core/device-id/device-id-protocol.ts @@ -1,9 +1,10 @@ -import { IssueCertificateResult, JWKPrivateSchema, SuperThis } from "@fireproof/core-types-base"; +import { IssueCertificateResult, JWKPrivateSchema, JWKPublic, SuperThis } from "@fireproof/core-types-base"; import { DeviceIdCA } from "./device-id-CA.js"; import { param, Result } from "@adviser/cement"; import { DeviceIdKey } from "./device-id-key.js"; import { base58btc } from "multiformats/bases/base58"; import { DeviceIdVerifyMsg, VerifyWithCertificateResult } from "./device-id-verify-msg.js"; +import { hashObjectAsync } from "@fireproof/core-runtime"; async function ensureCA(sthis: SuperThis, opts: DeviceIdProtocolSrvOpts): Promise> { const rEnv = sthis.env.gets({ @@ -29,7 +30,13 @@ async function ensureCA(sthis: SuperThis, opts: DeviceIdProtocolSrvOpts): Promis caSubject: { commonName: env.DEVICE_ID_CA_COMMON_NAME ?? "Fireproof CA", }, - actions: [], // opts.actions , + actions: { + async generateSerialNumber(pub: JWKPublic): Promise { + // const now = Date.now(); + const hash = await hashObjectAsync(pub); + return hash; + }, + }, }), ); } diff --git a/core/gateways/file-node/node-filesystem.ts b/core/gateways/file-node/node-filesystem.ts index 411c213d8..0bb6c5f6e 100644 --- a/core/gateways/file-node/node-filesystem.ts +++ b/core/gateways/file-node/node-filesystem.ts @@ -45,38 +45,3 @@ export class NodeFileSystem implements SysFileSystem { return this.fs?.writeFile(path, data); } } - -// import { type NodeMap, join } from "../../sys-container.js"; -// import type { ObjectEncodingOptions, PathLike } from "fs"; -// import * as fs from "fs/promises"; -// import * as path from "path"; -// import * as os from "os"; -// import * as url from "url"; -// import { toArrayBuffer } from "./utils.js"; - -// export async function createNodeSysContainer(): Promise { -// // const nodePath = "node:path"; -// // const nodeOS = "node:os"; -// // const nodeURL = "node:url"; -// // const nodeFS = "node:fs"; -// // const fs = (await import("node:fs")).promises; -// // const assert = "assert"; -// // const path = await import("node:path"); -// return { -// state: "node", -// ...path, -// // ...(await import("node:os")), -// // ...(await import("node:url")), -// ...os, -// ...url, -// ...fs, -// join, -// stat: fs.stat as NodeMap["stat"], -// readdir: fs.readdir as NodeMap["readdir"], -// readfile: async (path: PathLike, options?: ObjectEncodingOptions): Promise => { -// const rs = await fs.readFile(path, options); -// return toArrayBuffer(rs); -// }, -// writefile: fs.writeFile as NodeMap["writefile"], -// }; -// } diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index 101d2a2aa..1e36ed59e 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -1,5 +1,4 @@ import { Result } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; import { gte, and, eq, gt, inArray, lt, ne, or } from "drizzle-orm/sql/expressions"; // import type { LibSQLDatabase } from "drizzle-orm/libsql"; import { jwtVerify } from "jose"; @@ -62,6 +61,7 @@ import { sqlTokenByResultId } from "./token-by-result-id.js"; import { UserNotFoundError, getUser, isUserNotFound, queryUser, upsetUserByProvider } from "./users.js"; import { createFPToken, FPTokenContext, getFPTokenContext } from "./create-fp-token.js"; import { Role, ReadWrite, toRole, toReadWrite, FPCloudClaim } from "@fireproof/core-types-protocols-cloud"; +import { SuperThis } from "@fireproof/core-types-base"; import { sts } from "@fireproof/core-runtime"; import { DashSqlite } from "./create-handler.js"; diff --git a/dashboard/backend/create-fp-token.ts b/dashboard/backend/create-fp-token.ts index 696bb863f..a7abbba5f 100644 --- a/dashboard/backend/create-fp-token.ts +++ b/dashboard/backend/create-fp-token.ts @@ -1,5 +1,5 @@ import { param, Result } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; +import { SuperThis } from "@fireproof/core-types-base"; import { sts } from "@fireproof/core-runtime"; import { FPCloudClaim } from "@fireproof/core-types-protocols-cloud"; import { SignJWT } from "jose/jwt/sign"; diff --git a/dashboard/backend/create-handler.ts b/dashboard/backend/create-handler.ts index 124ea3e06..789b366e8 100644 --- a/dashboard/backend/create-handler.ts +++ b/dashboard/backend/create-handler.ts @@ -2,7 +2,7 @@ import { CoercedHeadersInit, HttpHeader, Lazy, LoggerImpl, Result, exception2Result, param } from "@adviser/cement"; import { verifyToken } from "@clerk/backend"; import { verifyJwt } from "@clerk/backend/jwt"; -import { SuperThis, SuperThisOpts } from "@fireproof/core"; +import { SuperThis, SuperThisOpts } from "@fireproof/core-types-base"; import { FPAPIMsg, FPApiSQL, FPApiToken } from "./api.js"; import type { Env } from "./cf-serve.js"; import { VerifiedAuth } from "@fireproof/core-protocols-dashboard"; diff --git a/dashboard/backend/db-api.test.ts b/dashboard/backend/db-api.test.ts index 829d35083..54e390b6f 100644 --- a/dashboard/backend/db-api.test.ts +++ b/dashboard/backend/db-api.test.ts @@ -7,7 +7,7 @@ // import { userRef } from "./db-api-schema"; import { Result } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; +import { SuperThis } from "@fireproof/core-types-base"; import { createClient } from "@libsql/client/node"; import { type LibSQLDatabase, drizzle } from "drizzle-orm/libsql"; import { jwtVerify } from "jose/jwt/verify"; diff --git a/dashboard/backend/invites.ts b/dashboard/backend/invites.ts index d7e909910..a2ebdf108 100644 --- a/dashboard/backend/invites.ts +++ b/dashboard/backend/invites.ts @@ -4,7 +4,7 @@ import { sqlUsers } from "./users.js"; import { sqlTenants } from "./tenants.js"; import { sqlLedgers } from "./ledgers.js"; import { queryEmail, queryNick, toUndef } from "./sql-helper.js"; -import { SuperThis } from "@fireproof/core"; +import { SuperThis } from "@fireproof/core-types-base"; import { AuthProvider, InvitedParams, diff --git a/dashboard/src/app-context.tsx b/dashboard/src/app-context.tsx index 98a62a7e0..5def32668 100644 --- a/dashboard/src/app-context.tsx +++ b/dashboard/src/app-context.tsx @@ -1,6 +1,6 @@ import React, { createContext, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import type { SuperThis } from "@fireproof/core"; +import type { SuperThis } from "@fireproof/core-types-base"; import { ensureSuperThis } from "@fireproof/core-runtime"; import { CloudContext } from "./cloud-context.js"; diff --git a/dashboard/src/components/DynamicTable.tsx b/dashboard/src/components/DynamicTable.tsx index c6a4a8270..3a37cdcb0 100644 --- a/dashboard/src/components/DynamicTable.tsx +++ b/dashboard/src/components/DynamicTable.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useNavigate } from "react-router-dom"; -import { DocBase, DocWithId } from "@fireproof/core"; +import { DocBase, DocWithId } from "@fireproof/core-types-base"; // export interface TableRow extends DocBase { // // readonly _id: string; diff --git a/dashboard/src/pages/databases.tsx b/dashboard/src/pages/databases.tsx index aede09994..36cafe3c3 100644 --- a/dashboard/src/pages/databases.tsx +++ b/dashboard/src/pages/databases.tsx @@ -2,7 +2,7 @@ import React, { useContext } from "react"; import { NavLink, useLoaderData, useNavigate } from "react-router-dom"; import { AppContext } from "../app-context.jsx"; import { SYNC_DB_NAME, truncateDbName } from "../helpers.js"; -import { fireproof } from "@fireproof/core"; +import { fireproof } from "@fireproof/core-base"; import { WithSidebar } from "../layouts/with-sidebar.jsx"; const reservedDbNames: string[] = [`fp.${SYNC_DB_NAME}`, "fp.petname_mappings", "fp.fp_sync"]; diff --git a/dashboard/src/pages/databases/connect.tsx b/dashboard/src/pages/databases/connect.tsx index 1b09f4950..b24035cbb 100644 --- a/dashboard/src/pages/databases/connect.tsx +++ b/dashboard/src/pages/databases/connect.tsx @@ -1,6 +1,6 @@ import React from "react"; import { redirect } from "react-router-dom"; -import { fireproof } from "@fireproof/core"; +import { fireproof } from "@fireproof/core-base"; import { DEFAULT_ENDPOINT, SYNC_DB_NAME } from "../../helpers.js"; import { URI } from "@adviser/cement"; diff --git a/dashboard/src/pages/databases/new.tsx b/dashboard/src/pages/databases/new.tsx index 66a4fc251..d4164fd5a 100644 --- a/dashboard/src/pages/databases/new.tsx +++ b/dashboard/src/pages/databases/new.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useForm } from "react-hook-form"; import { Form, redirect, SubmitTarget, useSubmit } from "react-router-dom"; -import { fireproof } from "@fireproof/core"; +import { fireproof } from "@fireproof/core-base"; export async function newDatabaseAction({ request }: { request: Request }) { const dbName = (await request.json<{ dbName: string }>()).dbName; diff --git a/dashboard/src/pages/databases/query.tsx b/dashboard/src/pages/databases/query.tsx index 3a9271d6c..c4e4d2e53 100644 --- a/dashboard/src/pages/databases/query.tsx +++ b/dashboard/src/pages/databases/query.tsx @@ -24,7 +24,7 @@ export function DatabasesQuery() { async function runTempQuery() { try { - // Try to evaluate the function to check for errors + // Try to evaluate the function to check for errors eval(`(${editorCode})`); setEditorCodeFnString(editorCode); setUserCodeError(null); From 9eddf473d5d6c4cd9a3e017e6762facdca24a9e2 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 27 Oct 2025 09:52:56 +0100 Subject: [PATCH 07/23] wip: just an snapshot [skip ci] --- .gitignore | 1 + .prettierignore | 1 + cloud/3rd-party/src/App.tsx | 4 +- core/gateways/cloud/to-cloud.ts | 4 +- core/gateways/file-node/node-filesystem.ts | 3 +- core/protocols/dashboard/msg-api.ts | 25 +- core/protocols/dashboard/msg-is.ts | 10 + core/protocols/dashboard/msg-types.ts | 64 +++- core/types/protocols/cloud/gateway-control.ts | 2 +- core/types/protocols/cloud/msg-types.zod.ts | 28 +- dashboard/backend/api.ts | 92 ++---- dashboard/backend/cf-serve.ts | 12 +- dashboard/backend/create-handler.ts | 51 ++- dashboard/backend/db-api.test.ts | 15 +- dashboard/backend/deno-serve.ts | 2 +- .../backend/get-cloud-pubkey-from-env.ts | 29 ++ dashboard/backend/tenants.ts | 8 +- dashboard/backend/well-known-jwks.ts | 32 +- dashboard/package.json | 2 - use-fireproof/fp-cloud-connect-strategy.ts | 18 +- .../clerk-fpcc-evt-entity.ts | 212 ++++++++++++ .../fp-cloud-connector/fp-cloud-connector.ts | 18 +- .../iframe-fpcc-protocol.ts | 308 +++++++++++------- .../fp-cloud-connector/injected-iframe.html | 2 +- .../page-fpcc-protocol.test.ts | 5 +- .../protocol-fp-cloud-conn.ts | 2 +- use-fireproof/redirect-strategy.ts | 6 +- 27 files changed, 695 insertions(+), 261 deletions(-) create mode 100644 dashboard/backend/get-cloud-pubkey-from-env.ts create mode 100644 use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts diff --git a/.gitignore b/.gitignore index 46bd52836..89a485382 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ logs *.tgz .npmrc +dist/** smoke/package.json smoke/pnpm-lock.yaml smoke/react/package.json diff --git a/.prettierignore b/.prettierignore index 032d2aeb6..0360df56a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ scripts/ **/.esm-cache/** **/dist/** **/coverage/** +dist/ diff --git a/cloud/3rd-party/src/App.tsx b/cloud/3rd-party/src/App.tsx index 57d0cc5ca..a2bf9c420 100644 --- a/cloud/3rd-party/src/App.tsx +++ b/cloud/3rd-party/src/App.tsx @@ -10,9 +10,9 @@ function App() { strategy: new FPCloudConnectStrategy({ // overlayCss: defaultOverlayCss, overlayHtml, + dashboardURI: "http://localhost:7370/fp/cloud/api/token", + cloudApiURI: "http://localhost:7370/api", }), - // dashboardURI: "http://localhost:7370/fp/cloud/api/token", - // tokenApiURI: "http://localhost:7370/api", // urls: { base: "fpcloud://localhost:8787?protocol=ws" }, // tenant: "3rd-party", // ledger: "vibes", diff --git a/core/gateways/cloud/to-cloud.ts b/core/gateways/cloud/to-cloud.ts index 8fd51d259..195f9259b 100644 --- a/core/gateways/cloud/to-cloud.ts +++ b/core/gateways/cloud/to-cloud.ts @@ -2,7 +2,7 @@ import { BuildURI, CoerceURI, URI, AppContext, KeyedResolvOnce, Lazy, Result } f import { Ledger } from "@fireproof/core-types-base"; import { FPCloudClaim, - FPCloudClaimParseSchema, + FPCloudClaimSchema, FPCloudUri, hashableFPCloudRef, ToCloudAttachable, @@ -36,7 +36,7 @@ export class SimpleTokenStrategy implements TokenStrategie { let claims: FPCloudClaim; try { const rawClaims = decodeJwt(jwk); - const rParse = FPCloudClaimParseSchema.safeParse(rawClaims); + const rParse = FPCloudClaimSchema.safeParse(rawClaims); if (rParse.success) { claims = rParse.data; } else { diff --git a/core/gateways/file-node/node-filesystem.ts b/core/gateways/file-node/node-filesystem.ts index 0bb6c5f6e..c44b6026d 100644 --- a/core/gateways/file-node/node-filesystem.ts +++ b/core/gateways/file-node/node-filesystem.ts @@ -16,7 +16,8 @@ export class NodeFileSystem implements SysFileSystem { }; async start(): Promise { - this.fs = await import("node:fs/promises"); + const fsPromise = "node:fs/promises"; + this.fs = await import(/* @vite-ignore */ fsPromise); return this; } async mkdir(path: PathLike, options?: { recursive: boolean }): Promise { diff --git a/core/protocols/dashboard/msg-api.ts b/core/protocols/dashboard/msg-api.ts index 1a11dec2f..3db118e31 100644 --- a/core/protocols/dashboard/msg-api.ts +++ b/core/protocols/dashboard/msg-api.ts @@ -1,8 +1,15 @@ -import { exception2Result, Logger, Result } from "@adviser/cement"; -import { ReqTokenByResultId, ResTokenByResultId } from "./msg-types.js"; +import { exception2Result, Lazy, Logger, Result } from "@adviser/cement"; +import { + ReqClerkPublishableKey, + ReqCloudDbToken, + ReqTokenByResultId, + ResClerkPublishableKey, + ResCloudDbToken, + ResTokenByResultId, +} from "./msg-types.js"; import { FAPIMsgImpl } from "./msg-is.js"; -export class Api { +export class DashApi { readonly apiUrl: string; readonly isser = new FAPIMsgImpl(); constructor(apiUrl: string) { @@ -27,6 +34,18 @@ export class Api { }); } + readonly getClerkPublishableKey = Lazy(async (req: Omit = {}) => { + const rRes = await this.request({ ...req, type: "reqClerkPublishableKey" }); + if (rRes.isErr()) { + throw rRes.Err(); + } + return rRes.unwrap(); + }); + + getCloudDbToken(req: Omit): Promise> { + return this.request({ ...req, type: "reqCloudDbToken" }); + } + async waitForToken(req: Omit, logger: Logger): Promise> { const rTokenByResultId = await this.request({ ...req, diff --git a/core/protocols/dashboard/msg-is.ts b/core/protocols/dashboard/msg-is.ts index 12da46b2a..2334339c1 100644 --- a/core/protocols/dashboard/msg-is.ts +++ b/core/protocols/dashboard/msg-is.ts @@ -18,6 +18,8 @@ import { ReqDeleteLedger, ResTokenByResultId, ReqExtendToken, + ReqClerkPublishableKey, + ResClerkPublishableKey, } from "./msg-types.js"; interface FPApiMsgInterface { @@ -35,6 +37,8 @@ interface FPApiMsgInterface { isCloudSessionToken(jso: unknown): jso is ReqCloudSessionToken; isReqTokenByResultId(jso: unknown): jso is ReqTokenByResultId; isResTokenByResultId(jso: unknown): jso is ResTokenByResultId; + isReqClerkPublishableKey(jso: unknown): jso is ReqClerkPublishableKey; + isResClerkPublishableKey(jso: unknown): jso is ResClerkPublishableKey; isListLedgersByUser(jso: unknown): jso is ReqListLedgersByUser; isCreateLedger(jso: unknown): jso is ReqCreateLedger; isUpdateLedger(jso: unknown): jso is ReqUpdateLedger; @@ -103,4 +107,10 @@ export class FAPIMsgImpl implements FPApiMsgInterface { isReqExtendToken(jso: unknown): jso is ReqExtendToken { return hasType(jso, "reqExtendToken"); } + isReqClerkPublishableKey(jso: unknown): jso is ReqClerkPublishableKey { + return hasType(jso, "reqClerkPublishableKey"); + } + isResClerkPublishableKey(jso: unknown): jso is ResClerkPublishableKey { + return hasType(jso, "resClerkPublishableKey"); + } } diff --git a/core/protocols/dashboard/msg-types.ts b/core/protocols/dashboard/msg-types.ts index b97a2610b..5fa38c4c7 100644 --- a/core/protocols/dashboard/msg-types.ts +++ b/core/protocols/dashboard/msg-types.ts @@ -1,3 +1,4 @@ +import { JWKPublic } from "@fireproof/core-types-base"; import { ReadWrite, Role, TenantLedger } from "@fireproof/core-types-protocols-cloud"; export type AuthProvider = "github" | "google" | "fp" | "invite-per-email"; @@ -124,13 +125,20 @@ export interface ResCreateTenant { readonly tenant: OutTenantParams; } -export interface InCreateTenantParams { +export interface FPApiParameters { + readonly cloudPublicKeys: JWKPublic[]; + readonly clerkPublishableKey: string; + readonly maxTenants: number; + readonly maxAdminUsers: number; + readonly maxMemberUsers: number; + readonly maxInvites: number; + readonly maxLedgers: number; +} + +export type InCreateTenantParams = { readonly name?: string; readonly ownerUserId: string; - readonly maxAdminUsers?: number; - readonly maxMemberUsers?: number; - readonly maxInvites?: number; -} +} & Partial; export interface ReqCreateTenant { readonly type: "reqCreateTenant"; @@ -405,6 +413,42 @@ export interface ResDeleteLedger { readonly type: "resDeleteLedger"; } +export interface ReqCloudDbToken { + readonly type: "reqCloudDbToken"; + readonly auth: AuthType; + readonly tenantId?: string; + readonly ledgerId?: string; + readonly localDbName: string; + readonly appId: string; + readonly deviceId: string; +} + +export interface ResCloudDbTokenBound { + readonly type: "resCloudDbToken"; + readonly status: "bound"; + readonly token: string; // JWT +} + +export interface ResCloudDbTokenNotBound { + readonly type: "resCloudDbToken"; + readonly status: "not-bound"; + readonly reason: string; +} + +export type ResCloudDbToken = ResCloudDbTokenBound | ResCloudDbTokenNotBound; + +export function isResCloudDbTokenBound(res: ResCloudDbToken): res is ResCloudDbTokenBound { + return res.status === "bound" && res.type === "resCloudDbToken"; +} +export function isResCloudDbTokenNotBound(res: ResCloudDbToken): res is ResCloudDbTokenNotBound { + return res.status === "not-bound" && res.type === "resCloudDbToken"; +} + +export interface GetCloudDbTokenResult { + readonly res: ResCloudDbToken; + readonly claims?: unknown; +} + export interface ReqCloudSessionToken { readonly type: "reqCloudSessionToken"; readonly auth: AuthType; @@ -422,6 +466,16 @@ export interface ReqTokenByResultId { readonly resultId: string; } +export interface ReqClerkPublishableKey { + readonly type: "reqClerkPublishableKey"; +} + +export interface ResClerkPublishableKey { + readonly type: "resClerkPublishableKey"; + readonly publishableKey: string; + readonly cloudPublicKeys: JWKPublic[]; // same as .well-known/jwks.json +} + export interface ResTokenByResultId { readonly type: "resTokenByResultId"; readonly status: "found" | "not-found"; diff --git a/core/types/protocols/cloud/gateway-control.ts b/core/types/protocols/cloud/gateway-control.ts index ffaea87e4..8e72e1b32 100644 --- a/core/types/protocols/cloud/gateway-control.ts +++ b/core/types/protocols/cloud/gateway-control.ts @@ -9,7 +9,7 @@ export interface ToCloudAttachable extends Attachable { export interface TokenAndClaims { readonly token: string; - readonly claims?: FPCloudClaim; + readonly claims: FPCloudClaim; // readonly exp: number; // readonly tenant?: string; // readonly ledger?: string; diff --git a/core/types/protocols/cloud/msg-types.zod.ts b/core/types/protocols/cloud/msg-types.zod.ts index 1bfdcc9c9..48f0f3555 100644 --- a/core/types/protocols/cloud/msg-types.zod.ts +++ b/core/types/protocols/cloud/msg-types.zod.ts @@ -28,11 +28,11 @@ export const FPCloudClaimSchema = JWTPayloadSchema.extend({ email: z.email(), nickname: z.string().optional(), provider: z.enum(["github", "google"]).optional(), - created: z.date(), + created: z.coerce.date(), tenants: z.array(TenantClaimSchema), ledgers: z.array(LedgerClaimSchema), selected: TenantLedgerSchema, -}); +}).readonly(); // Type inference from schemas export type Role = z.infer; @@ -42,15 +42,15 @@ export type LedgerClaim = z.infer; export type TenantLedger = z.infer; export type FPCloudClaim = z.infer; -// For parsing JWT payload with date transformation -export const FPCloudClaimParseSchema = JWTPayloadSchema.extend({ - userId: z.string(), - email: z.email(), - nickname: z.string().optional(), - provider: z.enum(["github", "google"]).optional(), - // Transform string to Date if needed (common in JWT parsing) - created: z.union([z.date(), z.string().transform((str) => new Date(str)), z.number().transform((num) => new Date(num))]), - tenants: z.array(TenantClaimSchema), - ledgers: z.array(LedgerClaimSchema), - selected: TenantLedgerSchema, -}); +// // For parsing JWT payload with date transformation +// export const FPCloudClaimParseSchema = JWTPayloadSchema.extend({ +// userId: z.string(), +// email: z.email(), +// nickname: z.string().optional(), +// provider: z.enum(["github", "google"]).optional(), +// // Transform string to Date if needed (common in JWT parsing) +// created: z.union([z.date(), z.string().transform((str) => new Date(str)), z.number().transform((num) => new Date(num))]), +// tenants: z.array(TenantClaimSchema), +// ledgers: z.array(LedgerClaimSchema), +// selected: TenantLedgerSchema, +// }); diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index 1e36ed59e..82fa093b6 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -52,6 +52,9 @@ import { UserStatus, VerifiedAuth, FAPIMsgImpl, + FPApiParameters, + ResClerkPublishableKey, + ReqClerkPublishableKey, } from "@fireproof/core-protocols-dashboard"; import { prepareInviteTicket, sqlInviteTickets, sqlToInviteTickets } from "./invites.js"; import { sqlLedgerUsers, sqlLedgers, sqlToLedgers } from "./ledgers.js"; @@ -224,10 +227,12 @@ export class FPApiSQL implements FPApiInterface { readonly db: DashSqlite; readonly tokenApi: Record; readonly sthis: SuperThis; - constructor(sthis: SuperThis, db: DashSqlite, tokenApi: Record) { + readonly params: FPApiParameters; + constructor(sthis: SuperThis, db: DashSqlite, tokenApi: Record, params: FPApiParameters) { this.db = db; this.tokenApi = tokenApi; this.sthis = sthis; + this.params = params; } private async _authVerifyAuth(req: { readonly auth: AuthType }): Promise> { @@ -313,9 +318,8 @@ export class FPApiSQL implements FPApiInterface { }, }; const rTenant = await this.insertTenant(authWithUserId, { + ...this.params, ownerUserId: userId, - maxAdminUsers: 5, - maxMemberUsers: 5, }); await this.addUserToTenant(this.db, { userName: nameFromAuth(undefined, authWithUserId), @@ -1407,6 +1411,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(new UserNotFoundError()); } const rTenant = await this.insertTenant(auth as ActiveUserWithUserId, { + ...this.params, ...req.tenant, ownerUserId: auth.user.userId, }); @@ -1427,7 +1432,10 @@ export class FPApiSQL implements FPApiInterface { }); } - private async insertTenant(auth: ActiveUserWithUserId, req: InCreateTenantParams): Promise> { + private async insertTenant( + auth: ActiveUserWithUserId, + req: InCreateTenantParams & FPApiParameters, + ): Promise> { const tenantId = this.sthis.nextId(12).str; const cnt = await this.db.$count(sqlTenants, eq(sqlTenants.ownerUserId, auth.user.userId)); if (cnt + 1 >= auth.user.maxTenants) { @@ -1440,9 +1448,10 @@ export class FPApiSQL implements FPApiInterface { tenantId, name: req.name ?? `my-tenant[${tenantId}]`, ownerUserId: auth.user.userId, - maxAdminUsers: req.maxAdminUsers ?? 5, - maxMemberUsers: req.maxMemberUsers ?? 5, - maxInvites: req.maxInvites ?? 10, + maxAdminUsers: req.maxAdminUsers, + maxMemberUsers: req.maxMemberUsers, + maxInvites: req.maxInvites, + maxLedgers: req.maxLedgers, createdAt: nowStr, updatedAt: nowStr, }) @@ -1875,6 +1884,14 @@ export class FPApiSQL implements FPApiInterface { }); } + async getClerkPublishableKey(_req: ReqClerkPublishableKey): Promise> { + return Result.Ok({ + type: "resClerkPublishableKey", + publishableKey: this.params.clerkPublishableKey, + cloudPublicKeys: this.params.cloudPublicKeys, + }); + } + // this is why to expensive --- why not kv or other simple storage async getTokenByResultId(req: ReqTokenByResultId): Promise> { const past = new Date(new Date().getTime() - 15 * 60 * 1000).toISOString(); @@ -1949,64 +1966,3 @@ function toProvider(i: ClerkVerifyAuth): FPCloudClaim["provider"] { } return "google"; } - -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// async attachUserToTenant(req: ReqAttachUserToTenant): Promise> { -// const maxTenants = await this.db.select({ -// maxTenants: users.maxTenants -// }).from(users).where(eq(users.userId, req.userId)).get() ?? { maxTenants: 5 } - -// const tendantCount = await this.db.$count(tenantUsers, -// and( -// eq(tenants.ownerUserId, req.userId), -// ne(tenantUsers.active, 0) -// )) - -// if (tendantCount >= maxTenants.maxTenants) { -// return Result.Err(`max tenants reached:${maxTenants.maxTenants}`) -// } - -// const now = new Date().toISOString(); -// const values = { -// userId: req.userId, -// tenantId: req.tenantId, -// name: req.name, -// active: 1, -// createdAt: now, -// updatedAt: now -// } -// const rRes = await this.db -// .insert(tenantUsers) -// .values(values) -// .onConflictDoNothing() -// .returning() -// .run() -// const res = rRes.toJSON()[0] -// return Result.Ok({ -// type: 'resAttachUserToTenant', -// name: req.name, -// tenant: { -// tenantId: res. -// name: req.name, -// ownerUserId: req.userId, -// adminUserIds: [], -// memberUserIds: [], -// maxAdminUsers: 5, -// maxMemberUsers: 5, -// createdAt: new Date(), -// updatedAt: new Date() -// }, -// userId: req.userId, -// role: req.role -// }) - -// // throw new Error("Method not implemented."); -// } -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// async listLedgersByTenant(req: ReqListLedgerByTenant): Promise { -// throw new Error("Method not implemented."); -// } -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// async attachUserToLedger(req: ReqAttachUserToLedger): Promise { -// throw new Error("Method not implemented."); -// } diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index e1ce7ec8e..e8f651c49 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -8,7 +8,17 @@ export interface Env { DB: D1Database; // CLERK_SECRET_KEY: string; ASSETS: Fetcher; + + MAX_TENANTS?: number; + MAX_ADMIN_USERS?: number; + MAX_MEMBER_USERS?: number; + MAX_INVITES?: number; + MAX_LEDGERS?: number; + + CLERK_PUBLISHABLE_KEY: string; + CLOUD_SESSION_TOKEN_PUBLIC: string; } + export default { async fetch(request: Request, env: Env) { const uri = URI.from(request.url); @@ -16,7 +26,7 @@ export default { switch (true) { case uri.pathname.startsWith("/api"): // console.log("cf-serve", request.url, env); - ares = createHandler(drizzle(env.DB), env)(request) as unknown as Promise; + ares = createHandler(drizzle(env.DB), env).then((fn) => fn(request) as unknown as Promise); break; case uri.pathname.startsWith("/.well-known/jwks.json"): diff --git a/dashboard/backend/create-handler.ts b/dashboard/backend/create-handler.ts index 789b366e8..35761b720 100644 --- a/dashboard/backend/create-handler.ts +++ b/dashboard/backend/create-handler.ts @@ -9,6 +9,7 @@ import { VerifiedAuth } from "@fireproof/core-protocols-dashboard"; import { ensureSuperThis, ensureLogger } from "@fireproof/core-runtime"; import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; import { ResultSet } from "@libsql/client"; +import { getCloudPubkeyFromEnv } from "./get-cloud-pubkey-from-env.js"; // import { jwtVerify } from "jose/jwt/verify"; // import { JWK } from "jose"; @@ -176,8 +177,25 @@ class ClerkApiToken implements FPApiToken { export type DashSqlite = BaseSQLiteDatabase<"async", ResultSet | D1Result, Record>; +function coerceInt(value: undefined | string | number, def: number): number { + if (!value) { + return def; + } + if (typeof value === "number") { + return value; + } + const n = parseInt(value); + if (isNaN(n)) { + return def; + } + return n; +} + // BaseSQLiteDatabase<'async', ResultSet, TSchema> -export function createHandler(db: T, env: Record | Env) { +export async function createHandler( + db: T, + env: Record | Env, +): Promise<(req: Request) => Promise> { // const stream = new utils.ConsoleWriterStream(); const sthis = ensureSuperThis({ logger: new LoggerImpl(), @@ -191,10 +209,29 @@ export function createHandler(db: T, env: Record); const logger = ensureLogger(sthis, "createHandler"); - const fpApi = new FPApiSQL(sthis, db, { - clerk: new ClerkApiToken(sthis), - // better: new BetterApiToken(sthis), - }); + if (!(env as Env).CLERK_PUBLISHABLE_KEY) { + throw new Error("CLERK_PUBLISHABLE_KEY is required in env"); + } + const rCloudPublicKey = await getCloudPubkeyFromEnv((env as Env).CLOUD_SESSION_TOKEN_PUBLIC, sthis); + if (rCloudPublicKey.isErr()) { + throw rCloudPublicKey.Err(); + } + const fpApi = new FPApiSQL( + sthis, + db, + { + clerk: new ClerkApiToken(sthis), + }, + { + cloudPublicKeys: [rCloudPublicKey.Ok()], + clerkPublishableKey: (env as Env).CLERK_PUBLISHABLE_KEY, + maxTenants: coerceInt(env.MAX_TENANTS, 10), + maxAdminUsers: coerceInt(env.MAX_ADMIN_USERS, 5), + maxMemberUsers: coerceInt(env.MAX_MEMBER_USERS, 5), + maxInvites: coerceInt(env.MAX_INVITES, 10), + maxLedgers: coerceInt(env.MAX_LEDGERS, 5), + }, + ); return async (req: Request): Promise => { const startTime = performance.now(); if (req.method === "OPTIONS") { @@ -277,6 +314,10 @@ export function createHandler(db: T, env: Record { beforeAll(async () => { const client = createClient({ url: `file://${process.cwd()}/dist/sqlite.db` }); db = drizzle(client); - fpApi = new FPApiSQL(sthis, db, { clerk: new TestApiToken(sthis) }); + fpApi = new FPApiSQL( + sthis, + db, + { clerk: new TestApiToken(sthis) }, + { + cloudPublicKeys: [], + clerkPublishableKey: "test-clerk-publishable-key", + maxTenants: 10, + maxAdminUsers: 5, + maxMemberUsers: 5, + maxInvites: 10, + maxLedgers: 5, + }, + ); data.push( ...Array(10) diff --git a/dashboard/backend/deno-serve.ts b/dashboard/backend/deno-serve.ts index 5790fde2c..efbd41dee 100644 --- a/dashboard/backend/deno-serve.ts +++ b/dashboard/backend/deno-serve.ts @@ -12,7 +12,7 @@ function getClient() { async function main() { Deno.serve({ port: 7370, - handler: createHandler(getClient(), Deno.env.toObject()), + handler: await createHandler(getClient(), Deno.env.toObject()), }); } diff --git a/dashboard/backend/get-cloud-pubkey-from-env.ts b/dashboard/backend/get-cloud-pubkey-from-env.ts new file mode 100644 index 000000000..487a2bf9f --- /dev/null +++ b/dashboard/backend/get-cloud-pubkey-from-env.ts @@ -0,0 +1,29 @@ +import { Result, toCryptoRuntime } from "@adviser/cement"; +import { ensureSuperThis, sts } from "@fireproof/core-runtime"; +import { JWKPublic, JWKPublicSchema, SuperThis, toJwksAlg } from "@fireproof/core-types-base"; +import { isArrayBuffer, isUint8Array } from "util/types"; + +export async function getCloudPubkeyFromEnv(cloudToken?: string, sthis: SuperThis = ensureSuperThis()): Promise> { + const cstPub = cloudToken ?? sthis.env.get("CLOUD_SESSION_TOKEN_PUBLIC"); + if (!cstPub) { + return Result.Err("no public key: env:CLOUD_SESSION_TOKEN_PUBLIC"); + } + const key = await sts.env2jwk(cstPub, "ES256", sthis); + const jwKey = await toCryptoRuntime().exportKey("jwk", key); + if (isUint8Array(jwKey) || isArrayBuffer(jwKey)) { + return Result.Err("invalid key: jwk is ArrayBuffer or Uint8Array"); + } + const rJwtPublicKey = JWKPublicSchema.safeParse({ + use: "sig", + // kty: ktyFromAlg(key.algorithm.name), + ...jwKey, + alg: toJwksAlg(key.algorithm.name, jwKey), + ext: undefined, + key_ops: undefined, + kid: undefined, + }); + if (!rJwtPublicKey.success) { + return Result.Err(rJwtPublicKey.error); + } + return Result.Ok(rJwtPublicKey.data); +} diff --git a/dashboard/backend/tenants.ts b/dashboard/backend/tenants.ts index a8dfb7865..5e63d70c3 100644 --- a/dashboard/backend/tenants.ts +++ b/dashboard/backend/tenants.ts @@ -7,10 +7,10 @@ export const sqlTenants = sqliteTable("Tenants", { ownerUserId: text() .notNull() .references(() => sqlUsers.userId), - maxAdminUsers: int().notNull().default(5), - maxMemberUsers: int().notNull().default(5), - maxInvites: int().notNull().default(10), - maxLedgers: int().notNull().default(5), + maxAdminUsers: int().notNull(), //.default(5), + maxMemberUsers: int().notNull(), //.default(5), + maxInvites: int().notNull(), // .default(10), + maxLedgers: int().notNull(), // .default(5), status: text().notNull().default("active"), statusReason: text().notNull().default("just created"), createdAt: text().notNull(), diff --git a/dashboard/backend/well-known-jwks.ts b/dashboard/backend/well-known-jwks.ts index ec7b8f7cc..3a8d5269d 100644 --- a/dashboard/backend/well-known-jwks.ts +++ b/dashboard/backend/well-known-jwks.ts @@ -1,37 +1,17 @@ -import { Lazy, toCryptoRuntime } from "@adviser/cement"; -import { ensureSuperThis, sts } from "@fireproof/core-runtime"; -import { JWKPublicSchema, toJwksAlg } from "@fireproof/core-types-base"; -import { isArrayBuffer, isUint8Array } from "util/types"; +import { Lazy } from "@adviser/cement"; +import { getCloudPubkeyFromEnv } from "./get-cloud-pubkey-from-env.js"; const getKey = Lazy(async (opts: Record) => { - const sthis = ensureSuperThis(); - const cstPub = opts.CLOUD_SESSION_TOKEN_PUBLIC ?? sthis.env.get("CLOUD_SESSION_TOKEN_PUBLIC"); - if (!cstPub) + const rJwtPublicKey = await getCloudPubkeyFromEnv(opts.CLOUD_SESSION_TOKEN_PUBLIC); + if (rJwtPublicKey.isErr()) { return { status: 500, - value: { error: "no public key: env:CLOUD_SESSION_TOKEN_PUBLIC" }, - }; - - const key = await sts.env2jwk(cstPub, "ES256", sthis); - const jwKey = await toCryptoRuntime().exportKey("jwk", key); - if (isUint8Array(jwKey) || isArrayBuffer(jwKey)) { - return { - status: 500, - value: { error: "invalid key is not a CTJsonWebKey" }, + value: { keys: [] }, }; } - const jwPublicKey = JWKPublicSchema.parse({ - use: "sig", - // kty: ktyFromAlg(key.algorithm.name), - ...jwKey, - alg: toJwksAlg(key.algorithm.name, jwKey), - ext: undefined, - key_ops: undefined, - kid: undefined, - }); return { status: 200, - value: { keys: [jwPublicKey] }, + value: { keys: [rJwtPublicKey.Ok()] }, }; }); diff --git a/dashboard/package.json b/dashboard/package.json index e44b71ab3..5efab1b65 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -10,10 +10,8 @@ "deploy:cf": "wrangler deploy -c wrangler.toml", "build": "core-cli tsc && pnpm run build:vite", "build:vite": "vite build", - "lint": "eslint .", "preview": "vite preview", "test": "vitest --run", - "format": "prettier --config ../.prettierrc .", "check": "pnpm format --write && core-cli tsc --noEmit && pnpm lint && pnpm test && pnpm build", "drizzle:libsql": "drizzle-kit push --config ./drizzle.libsql.config.ts", "drizzle:d1-local": "drizzle-kit push --config ./drizzle.d1-local-backend.config.ts", diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index f50e7a093..2df91874c 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,4 +1,4 @@ -import { KeyedResolvOnce, Lazy, Logger, ResolveSeq, Result, URI } from "@adviser/cement"; +import { BuildURI, KeyedResolvOnce, Lazy, Logger, ResolveSeq, Result, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; import { ensureLogger, ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; @@ -12,6 +12,8 @@ import DOMPurify from "dompurify"; import { dbAppKey } from "./fp-cloud-connector/iframe-fpcc-protocol.js"; export interface FPCloudConnectOpts extends RedirectStrategyOpts { + readonly dashboardURI?: string; + readonly cloudApiURI?: string; readonly fpCloudConnectURL: string; readonly title?: string; readonly sthis?: SuperThis; @@ -54,10 +56,18 @@ export class FPCloudConnectStrategy implements TokenStrategie { constructor(opts: Partial = {}) { this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; - this.fpCloudConnectURL = + const fpCloudConnectURL = BuildURI.from( opts.fpCloudConnectURL ?? - // eslint-disable-next-line no-restricted-globals - new URL("fp-cloud-connector/injected-iframe.html", import.meta.url).toString(); + // eslint-disable-next-line no-restricted-globals + new URL("fp-cloud-connector/injected-iframe.html", import.meta.url).toString(), + ); + if (opts.dashboardURI) { + fpCloudConnectURL.setParam("dashboard_uri", opts.dashboardURI); + } + if (opts.cloudApiURI) { + fpCloudConnectURL.setParam("cloud_api_uri", opts.cloudApiURI); + } + this.fpCloudConnectURL = fpCloudConnectURL.toString(); this.title = opts.title ?? "Fireproof Login"; this.sthis = opts.sthis ?? ensureSuperThis(); this.logger = ensureLogger(this.sthis, "FPCloudConnectStrategy"); diff --git a/use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts b/use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts new file mode 100644 index 000000000..038cc5f61 --- /dev/null +++ b/use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts @@ -0,0 +1,212 @@ +import { Lazy, Logger, poller, Result } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core-types-base"; +import { DashApi, AuthType, isResCloudDbTokenBound } from "@fireproof/core-protocols-dashboard"; +import { BackendFPCC, convertToTokenAndClaims, DbKey, GetCloudDbTokenResult } from "./iframe-fpcc-protocol.js"; +import { ensureLogger, exceptionWrapper, sleep } from "@fireproof/core-runtime"; +import { FPCCEvtApp, FPCCMsgBase } from "./protocol-fp-cloud-conn.js"; +import { TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { Clerk } from "@clerk/clerk-js"; + +const clerkSvc = Lazy(async (dashApi: DashApi) => { + const clerkPubKey = await dashApi.getClerkPublishableKey({}); + // console.log("clerkSvc got publishable key", rClerkPubKey); + const clerk = new Clerk(clerkPubKey.publishableKey); + await clerk.load(); + clerk.addListener((session) => { + if (session.user) { + console.log("Iframe-Clerk-User signed in:", session.user); + } else { + console.log("Iframe-Clerk-User signed out"); + } + }); + + return clerk; +}); + +// const clerkFPCCEvtEntities = new KeyedResolvOnce(); + +export class ClerkFPCCEvtEntity implements BackendFPCC { + readonly appId: string; + readonly dbName: string; + readonly deviceId: string; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly dashApi: DashApi; + state: "needs-login" | "waiting" | "ready" = "needs-login"; + constructor(sthis: SuperThis, dashApi: DashApi, dbKey: DbKey, deviceId: string) { + this.logger = ensureLogger(sthis, `MemoryFPCCEvtEntity`, { + appId: dbKey.appId, + dbName: dbKey.dbName, + deviceId: deviceId, + }); + this.appId = dbKey.appId; + this.dbName = dbKey.dbName; + this.deviceId = deviceId; + this.sthis = sthis; + this.dashApi = dashApi; + } + + async getCloudDbToken(auth: AuthType): Promise> { + const rRes = await this.dashApi.getCloudDbToken({ + auth, + appId: this.appId, + localDbName: this.dbName, + deviceId: this.deviceId, + }); + if (rRes.isErr()) { + return Result.Err(rRes); + } + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + return Result.Ok({ res }); + } + const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); + if (rTandC.isErr()) { + return Result.Err(rTandC); + } + return Result.Ok({ res, claims: rTandC.Ok().claims }); + } + + isUserLoggedIn(): Promise { + return clerkSvc(this.dashApi).then((clerk) => { + return !!clerk.user; + }); + } + + async getDashApiToken(): Promise> { + return exceptionWrapper(async () => { + const clerk = await clerkSvc(this.dashApi); + if (!clerk.user) { + return Result.Err(new Error("User not logged in")); + } + const token = await clerk.session?.getToken(); + if (!token) { + return Result.Err(new Error("No session token available")); + } + return Result.Ok({ + type: "clerk", + token, + }); + }); + } + + isFPCCEvtAppReady(): boolean { + // need to implement a check which looks into the token if it is expired or not + return this.state === "ready"; + } + + fpccEvtApp?: FPCCEvtApp; + getFPCCEvtApp(): Promise> { + return Promise.resolve(this.fpccEvtApp ? Result.Ok(this.fpccEvtApp) : Result.Err(new Error("No FPCCEvtApp registered"))); + } + + setFPCCEvtApp(app: FPCCEvtApp): Promise { + this.fpccEvtApp = app; + return Promise.resolve(); + } + getState(): "needs-login" | "waiting" | "ready" { + // For testing purposes, we always return "needs-login" + return this.state; + } + + // listRegisteredDbNames(): Promise { + // return Promise.all( + // clerkFPCCEvtEntities + // .values() + // .map((key) => { + // return key.value; + // }) + // .filter((v) => v.isOk()) + // .map((v) => v.Ok().getFPCCEvtApp()), + // ).then((apps) => { + // console.log("listRegisteredDbNames-o", apps); + // return apps.filter((res) => res.isOk()).map((res) => res.Ok()); + // }); + // } + + setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready" { + const prev = this.state; + this.state = state; + return prev; + } + + // + async waitForAuthToken(resultId: string): Promise> { + return poller(async () => { + const clerk = await clerkSvc(this.dashApi); + if (!clerk.user) { + return { + state: "waiting", + }; + } + // console.log("clerk user is logged in:", clerk.user); + + const rWaitForToken = await this.dashApi.waitForToken({ resultId }, this.logger); + if (rWaitForToken.isErr()) { + return { + state: "error", + error: rWaitForToken.Err(), + }; + } + const waitedTokenByResultId = rWaitForToken.unwrap(); + if (waitedTokenByResultId.status === "found" && waitedTokenByResultId.token) { + const token = waitedTokenByResultId.token; + if (!token) { + return { + state: "error", + error: new Error("No token received"), + }; + } + const rTokenClaims = await convertToTokenAndClaims(this.dashApi, this.logger, token); + if (rTokenClaims.isErr()) { + return { + state: "error", + error: rTokenClaims.Err(), + }; + } + return { + state: "success", + result: rTokenClaims.Ok(), + }; + } + return { state: "waiting" }; + }).then((res) => { + switch (res.state) { + case "success": + return Result.Ok(res.result); + case "error": + return Result.Err(res.error); + default: + return Result.Err("should not happen"); + } + }); + } + + async getTokenForDb(dbInfo: DbKey, authToken: TokenAndClaims, originEvt: Partial): Promise { + await sleep(50); + return { + ...dbInfo, + tid: originEvt.tid ?? this.sthis.nextId(12).str, + type: "FPCCEvtApp", + src: "fp-cloud-connector", + dst: originEvt.src ?? "iframe", + appFavIcon: { + defURL: "https://example.com/favicon.ico", + }, + devId: this.deviceId, + user: { + name: "Test User", + email: "test@example.com", + provider: "google", + iconURL: "https://example.com/icon.png", + }, + localDb: { + dbName: dbInfo.dbName, + tenantId: "tenant-for-" + dbInfo.appId, + ledgerId: "ledger-for-" + dbInfo.appId, + accessToken: `auth-token-for-${dbInfo.appId}-${dbInfo.dbName}-with-${authToken}`, + }, + env: {}, + }; + } +} diff --git a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts index daaab84d1..293a62054 100644 --- a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts +++ b/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts @@ -1,14 +1,26 @@ import { ensureSuperThis } from "@fireproof/core-runtime"; -import { Lazy } from "@adviser/cement"; +import { BuildURI, Lazy, URI } from "@adviser/cement"; import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; -export const postMessager = Lazy(async () => { +export const fpCloudConnector = Lazy(async (loadUrlStr: string) => { (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { FP_DEBUG: "*", }; const sthis = ensureSuperThis(); - const protocol = new IframeFPCCProtocol(sthis); + const loadUrl = URI.from(loadUrlStr); + const dashboardURI = loadUrl.getParam("dashboard_uri"); + let cloudApiURI = loadUrl.getParam("cloud_api_uri"); + if (dashboardURI && !cloudApiURI) { + cloudApiURI = BuildURI.from(dashboardURI).pathname("/api").toString(); + } + + console.log("fpCloudConnector called with", loadUrlStr, { dashboardURI, cloudApiURI }); + + const protocol = new IframeFPCCProtocol(sthis, { + dashboardURI: dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud", + cloudApiURI: cloudApiURI ?? "https://dev.connect.fireproof.direct/api", + }); window.addEventListener("message", protocol.handleMessage); protocol.injectSend((event: FPCCMessage, srcEvent: MessageEvent) => { (event as { src: string }).src = event.src ?? window.location.href; diff --git a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts b/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts index 9866c8d58..3cd4e5cee 100644 --- a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts +++ b/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts @@ -12,12 +12,22 @@ import { isFPCCReqWaitConnectorReady, } from "./protocol-fp-cloud-conn.js"; import { SuperThis } from "@fireproof/core-types-base"; -import { BuildURI, KeyedResolvOnce, Logger, Result } from "@adviser/cement"; +import { BuildURI, exception2Result, KeyedResolvSeq, Logger, Result } from "@adviser/cement"; +import { + DashApi, + AuthType, + ResCloudDbTokenBound, + ResCloudDbTokenNotBound, + isResCloudDbTokenBound, +} from "@fireproof/core-protocols-dashboard"; +import { FPCloudClaimSchema, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { ClerkFPCCEvtEntity } from "./clerk-fpcc-evt-entity.js"; +import { jwtVerify } from "jose"; -interface IframeFPCCProtocolOpts { - dashboardURI: string; - waitForTokenURI: string; - backend: BackendFPCC; +export interface IframeFPCCProtocolOpts { + readonly dashboardURI: string; + readonly cloudApiURI: string; + // readonly backend: BackendFPCC; } export interface DbKey { @@ -29,102 +39,65 @@ export function dbAppKey(o: DbKey): string { return o.appId + ":" + o.dbName; } -interface BackendFPCC { +export type GetCloudDbTokenResult = + | { + readonly res: ResCloudDbTokenBound; + readonly claims: TokenAndClaims["claims"]; + } + | { + readonly res: ResCloudDbTokenNotBound; + }; +export interface BackendFPCC { + readonly appId: string; + readonly dbName: string; + readonly deviceId: string; + isFPCCEvtAppReady(): boolean; getState(): "needs-login" | "waiting" | "ready"; setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready"; - waitForAuthToken(tid: string, tokenURI: string): Promise; + waitForAuthToken(resultId: string): Promise>; getFPCCEvtApp(): Promise>; setFPCCEvtApp(app: FPCCEvtApp): Promise; - listRegisteredDbNames(): Promise; - getTokenForDb(dbInfo: DbKey, authToken: string, src: Partial): Promise; -} - -function getBackendFromRegisterLocalDbName(sthis: SuperThis, req: DbKey, deviceId: string): BackendFPCC { - return MemoryFPCCEvtEntity.fromRegisterLocalDbName(sthis, req, deviceId); + isUserLoggedIn(): Promise; + getDashApiToken(): Promise>; + // listRegisteredDbNames(): Promise; + getCloudDbToken(auth: AuthType): Promise>; } -const memoryFPCCEvtEntities = new KeyedResolvOnce(); -class MemoryFPCCEvtEntity implements BackendFPCC { - static fromRegisterLocalDbName(sthis: SuperThis, req: DbKey, deviceId: string): MemoryFPCCEvtEntity { - const key = dbAppKey(req); - return memoryFPCCEvtEntities.get(key).once(() => new MemoryFPCCEvtEntity(sthis, req, deviceId)); - } - readonly dbKey: DbKey; - readonly deviceId: string; - readonly sthis: SuperThis; - state: "needs-login" | "waiting" | "ready" = "needs-login"; - constructor(sthis: SuperThis, dbKey: DbKey, deviceId: string) { - this.dbKey = dbKey; - this.deviceId = deviceId; - this.sthis = sthis; - } +// function getBackendFromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): BackendFPCC { +// return ClerkFPCCEvtEntity.fromRegisterLocalDbName(sthis, dashApi, req, deviceId); +// } - fpccEvtApp?: FPCCEvtApp; - getFPCCEvtApp(): Promise> { - return Promise.resolve(this.fpccEvtApp ? Result.Ok(this.fpccEvtApp) : Result.Err(new Error("No FPCCEvtApp registered"))); - } +const registeredDbs = new Map(); - setFPCCEvtApp(app: FPCCEvtApp): Promise { - this.fpccEvtApp = app; - return Promise.resolve(); - } - getState(): "needs-login" | "waiting" | "ready" { - // For testing purposes, we always return "needs-login" - return this.state; - } +// static fromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): ClerkFPCCEvtEntity { +// const key = dbAppKey(req); +// return clerkFPCCEvtEntities.get(key).once(() => new ClerkFPCCEvtEntity(sthis, dashApi, req, deviceId)); +// } - listRegisteredDbNames(): Promise { - return Promise.all( - memoryFPCCEvtEntities - .values() - .map((key) => { - return key.value; +export async function convertToTokenAndClaims(dashApi: DashApi, logger: Logger, token: string): Promise> { + for (const jwkPublic of await dashApi.getClerkPublishableKey().then((r) => r.cloudPublicKeys)) { + const rUnknownClaims = await exception2Result(() => jwtVerify(token, jwkPublic)); + if (rUnknownClaims.isErr() || !rUnknownClaims.Ok()?.payload) { + logger + .Warn() + .Err(rUnknownClaims) + .Any({ + kid: jwkPublic.kid, }) - .filter((v) => v.isOk()) - .map((v) => v.Ok().getFPCCEvtApp()), - ).then((apps) => { - console.log("listRegisteredDbNames-o", apps); - return apps.filter((res) => res.isOk()).map((res) => res.Ok()); + .Msg("Token failed"); + continue; + } + const rFPCloudClaim = FPCloudClaimSchema.safeParse(rUnknownClaims.Ok().payload); + if (!rFPCloudClaim.success) { + logger.Warn().Err(rFPCloudClaim.error).Msg("Token claims validation failed"); + continue; + } + return Result.Ok({ + token, + claims: rFPCloudClaim.data, }); } - - setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready" { - const prev = this.state; - this.state = state; - return prev; - } - - waitForAuthToken(tid: string, tokenURI: string): Promise { - return sleep(100).then(() => `fake-auth-token:${tid}:${tokenURI}`); - } - - async getTokenForDb(dbInfo: DbKey, authToken: string, originEvt: Partial): Promise { - await sleep(50); - return { - ...dbInfo, - tid: originEvt.tid ?? this.sthis.nextId(12).str, - type: "FPCCEvtApp", - src: "fp-cloud-connector", - dst: originEvt.src ?? "iframe", - appFavIcon: { - defURL: "https://example.com/favicon.ico", - }, - devId: this.deviceId, - user: { - name: "Test User", - email: "test@example.com", - provider: "google", - iconURL: "https://example.com/icon.png", - }, - localDb: { - dbName: dbInfo.dbName, - tenantId: "tenant-for-" + dbInfo.appId, - ledgerId: "ledger-for-" + dbInfo.appId, - accessToken: `auth-token-for-${dbInfo.appId}-${dbInfo.dbName}-with-${authToken}`, - }, - env: {}, - }; - } + return Result.Err("No valid JWK found to verify token"); } export class IframeFPCCProtocol implements FPCCProtocol { @@ -132,14 +105,28 @@ export class IframeFPCCProtocol implements FPCCProtocol { readonly logger: Logger; readonly fpccProtocol: FPCCProtocolBase; readonly dashboardURI: string; - readonly waitForTokenURI: string; + readonly dashApiURI: string; + readonly dashApi: DashApi; - constructor(sthis: SuperThis, opts: Partial = {}) { + constructor(sthis: SuperThis, opts: IframeFPCCProtocolOpts) { this.sthis = sthis; this.logger = ensureLogger(sthis, "IframeFPCCProtocol"); this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); this.dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud"; - this.waitForTokenURI = opts.waitForTokenURI ?? "https://dev.connect.fireproof.direct/api"; + this.dashApiURI = opts.cloudApiURI ?? "https://dev.connect.fireproof.direct/api"; + console.log("IframeFPCCProtocol constructed with", opts); + this.dashApi = new DashApi(this.dashApiURI); + } + + registeredDb(key: DbKey) { + const mapKey = dbAppKey(key); + const existing = registeredDbs.get(mapKey); + if (existing) { + return existing; + } + const newEntity = new ClerkFPCCEvtEntity(this.sthis, this.dashApi, key, this.getDeviceId()); + registeredDbs.set(mapKey, newEntity); + return newEntity; } readonly handleMessage = (event: MessageEvent): void => { @@ -150,11 +137,12 @@ export class IframeFPCCProtocol implements FPCCProtocol { return "we-need-to-implement-device-id"; } - async needsLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { + async requestPageToDoLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { const loginTID = this.sthis.nextId(16).str; const url = BuildURI.from(this.dashboardURI) - .setParam("back_url", "close") + .setParam("back_url", "wait-for-token") // dummy back_url since we don't return to the app here .setParam("result_id", loginTID) + .setParam("app_id", event.appId) .setParam("local_ledger_name", event.dbName); if (event.ledger) { url.setParam("ledger", event.ledger); @@ -169,25 +157,18 @@ export class IframeFPCCProtocol implements FPCCProtocol { devId: this.getDeviceId(), loginURL: url.toString(), loginTID, - loadDbNames: [ - ...(await backend.listRegisteredDbNames().then((apps) => - apps.map((app) => ({ - appId: app.appId, - dbName: app.localDb.dbName, - })), - )), - event, - ], + loadDbNames: [event], reason: "BindCloud", }; - for (const dbInfo of fpccEvtNeedsLogin.loadDbNames) { - const backend = getBackendFromRegisterLocalDbName(this.sthis, dbInfo, this.getDeviceId()); - backend.setState("waiting"); - } + this.sendMessage(fpccEvtNeedsLogin, srcEvent); - backend.waitForAuthToken(loginTID, this.waitForTokenURI).then((authToken) => { + backend.waitForAuthToken(loginTID).then((rAuthToken) => { + if (rAuthToken.isErr()) { + this.logger.Error().Err(rAuthToken).Msg("Failed to obtain auth token after login"); + return; + } return Promise.allSettled( - fpccEvtNeedsLogin.loadDbNames.map(async (dbInfo) => backend.getTokenForDb(dbInfo, authToken, event)), + fpccEvtNeedsLogin.loadDbNames.map(async (dbInfo) => backend.getCloudDbToken(rAuthToken.Ok().token)), ).then((results) => { results.forEach((res) => { if (res.status === "fulfilled") { @@ -204,7 +185,16 @@ export class IframeFPCCProtocol implements FPCCProtocol { }); } + readonly stateSeq = new KeyedResolvSeq(); runStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { + return this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + } + + listRegisteredDbs(): BackendFPCC[] { + return Array.from(registeredDbs.values()); + } + + async atomicRunStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { const bstate = backend.getState(); switch (true) { case bstate === "ready" && isFPCCReqRegisterLocalDbName(event): @@ -227,8 +217,101 @@ export class IframeFPCCProtocol implements FPCCProtocol { } break; case bstate === "needs-login" && isFPCCReqRegisterLocalDbName(event): - console.log("Backend needs login"); - return this.needsLogin(backend, event, srcEvent); + { + console.log("Backend needs login", backend.appId, backend.dbName); + const rAuthToken = await backend.getDashApiToken(); + if (rAuthToken.isErr()) { + console.log("User not logged in, requesting login", backend.appId, backend.dbName); + // make all dbs go to waiting state + backend.setState("waiting"); + return this.requestPageToDoLogin(backend, event, srcEvent); + } else { + // const backend = this.registeredDb(event); + + if (backend.isFPCCEvtAppReady()) { + const rFpccEvtApp = await backend.getFPCCEvtApp(); + console.log("Backend is ready, sending FPCCEvtApp", backend.appId, backend.dbName, rFpccEvtApp); + if (rFpccEvtApp.isOk()) { + this.sendMessage(rFpccEvtApp.Ok(), srcEvent); + return; + } + } else { + const rDbToken = await this.dashApi.getCloudDbToken({ + auth: rAuthToken.Ok(), + appId: backend.appId, + localDbName: backend.dbName, + deviceId: backend.deviceId, + }); + if (rDbToken.isErr()) { + console.log("Failed to obtain DB token, requesting login", backend.appId, backend.dbName, rDbToken); + // make all dbs go to waiting state + backend.setState("waiting"); + await sleep(60000); + this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + return; + } + if (rDbToken.Ok().status === "not-bound") { + console.log("DB is not bound, requesting login", backend.appId, backend.dbName); + // make all dbs go to waiting state + backend.setState("waiting"); + return this.requestPageToDoLogin(backend, event, srcEvent); + } else { + const rCloudToken = await backend.getCloudDbToken(rAuthToken.Ok()); + if (rCloudToken.isErr()) { + this.logger.Warn().Err(rCloudToken).Msg("Failed to obtain DB token, re-running state machine after delay"); + await sleep(1000); + this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + return; + } + const res = rCloudToken.Ok().res; + if (!isResCloudDbTokenBound(res)) { + return this.requestPageToDoLogin(backend, event, srcEvent); + } + const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); + if (rTandC.isErr()) { + this.logger + .Warn() + .Err(rTandC) + .Msg("Failed to convert DB token to token and claims, re-running state machine after delay"); + await sleep(1000); + this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + return; + } + const { token, claims } = rTandC.Ok(); + const fpccEvtApp: FPCCEvtApp = { + tid: event.tid, + type: "FPCCEvtApp", + src: "iframe", + dst: event.src, + appId: backend.appId, + appFavIcon: { + defURL: "https://fireproof.direct/favicon.ico", + }, + devId: backend.deviceId, + user: { + name: claims.nickname ?? claims.userId, + email: claims.email, + provider: claims.provider ?? "unknown", + iconURL: "https://fireproof.direct/favicon.ico", + }, + localDb: { + dbName: backend.dbName, + tenantId: claims.selected.tenant, + ledgerId: claims.selected.ledger, + accessToken: token, + }, + env: {}, + }; + await backend.setFPCCEvtApp(fpccEvtApp); + backend.setState("ready"); + console.log("Sent FPCCEvtApp after obtaining DB token", backend.appId, backend.dbName); + this.sendMessage(fpccEvtApp, srcEvent); + return; + } + } + } + } + break; default: throw this.logger.Error().Str("state", bstate).Msg("Unknown backend state").AsError(); @@ -240,7 +323,8 @@ export class IframeFPCCProtocol implements FPCCProtocol { switch (true) { case isFPCCReqRegisterLocalDbName(event): { this.logger.Info().Any(event).Msg("Iframe-Received request to register app"); - const backend = getBackendFromRegisterLocalDbName(this.sthis, event, this.getDeviceId()); + const backend = this.registeredDb(event); + backend.setState("needs-login"); console.log("Running state machine for register local db name", backend.getState()); this.runStateMachine(backend, event, srcEvent); break; diff --git a/use-fireproof/fp-cloud-connector/injected-iframe.html b/use-fireproof/fp-cloud-connector/injected-iframe.html index c674ecb6d..18b636c33 100644 --- a/use-fireproof/fp-cloud-connector/injected-iframe.html +++ b/use-fireproof/fp-cloud-connector/injected-iframe.html @@ -15,7 +15,7 @@ fpcc = await import(url); console.log("loaded -- ts", url.toString(), fpcc); } - fpcc.postMessager().then(() => console.log("injected-iframe-ready")); + fpcc.fpCloudConnector(window.location.href).then(() => console.log("injected-iframe-ready", window.location.href)); diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts index 7a2d8332e..a043ae1cf 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts +++ b/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts @@ -11,7 +11,10 @@ describe("FPCC Protocol", () => { iframeHref: "https://example.com/iframe", loginWaitTime: 1000, }); - const iframeProtocol = new IframeFPCCProtocol(sthis); + const iframeProtocol = new IframeFPCCProtocol(sthis, { + dashboardURI: "https://example.com/dashboard", + cloudApiURI: "https://example.com/wait-for-token", + }); iframeProtocol.injectSend((evt: Writable) => { evt.src = evt.src ?? "iframe"; diff --git a/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts b/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts index aae924f6c..6a17f6f00 100644 --- a/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts +++ b/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts @@ -69,7 +69,7 @@ export const FPCCEvtAppSchema = FPCCMsgBaseSchemaBase.extend({ .object({ name: z.string(), email: z.string(), - provider: z.enum(["google", "github"]), + provider: z.enum(["google", "github", "unknown"]), iconURL: z.string(), }) .readonly(), diff --git a/use-fireproof/redirect-strategy.ts b/use-fireproof/redirect-strategy.ts index ba07c2e87..de813823a 100644 --- a/use-fireproof/redirect-strategy.ts +++ b/use-fireproof/redirect-strategy.ts @@ -3,7 +3,7 @@ import { SuperThis } from "@fireproof/core-types-base"; import { decodeJwt } from "jose"; import DOMPurify from "dompurify"; import { FPCloudClaim, ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { Api } from "@fireproof/core-protocols-dashboard"; +import { DashApi } from "@fireproof/core-protocols-dashboard"; import { WebToCloudCtx } from "./react/types.js"; import { WebCtx } from "./react/use-attach.js"; import { hashObjectSync } from "@fireproof/core-runtime"; @@ -120,7 +120,7 @@ export class RedirectStrategy implements TokenStrategie { async getTokenAndClaimsByResultId( logger: Logger, - dashApi: Api, + dashApi: DashApi, resultId: undefined | string, opts: ToCloudOpts, resolve: (value: TokenAndClaims) => void, @@ -157,7 +157,7 @@ export class RedirectStrategy implements TokenStrategie { throw new Error("waitForToken not working on redirect strategy"); } const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; - const dashApi = new Api(webCtx.tokenApiURI); + const dashApi = new DashApi(webCtx.tokenApiURI); this.waitState = "started"; return new Promise>((resolve) => { this.getTokenAndClaimsByResultId(logger, dashApi, this.resultId, opts, (tokenAndClaims) => { From d90d58780f10df869e7d32182cad53c82e66704e Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 27 Oct 2025 22:40:13 +0100 Subject: [PATCH 08/23] wip: refactor into new structure --- .../base/convert-to-token-and-claims.ts | 36 ++ .../connector/base}/fpcc-protocol.ts | 0 cloud/connector/base/index.ts | 13 + cloud/connector/base/package.json | 41 ++ .../connector/base}/post-messager.ts | 0 .../connector/base}/protocol-fp-cloud-conn.ts | 0 .../iframe}/clerk-fpcc-evt-entity.ts | 6 +- .../connector/iframe}/fp-cloud-connector.ts | 2 +- .../connector/iframe}/iframe-fpcc-protocol.ts | 135 +++--- cloud/connector/iframe/index.ts | 3 + .../connector/iframe}/injected-iframe.html | 0 cloud/connector/iframe/package.json | 42 ++ cloud/connector/page/index.ts | 2 + cloud/connector/page/package.json | 40 ++ .../connector/page}/page-fpcc-protocol.ts | 8 +- .../connector/page}/page-handler.ts | 4 +- cloud/connector/test/package.json | 42 ++ .../test}/page-fpcc-protocol.test.ts | 72 +-- cloud/connector/test/vitest.config.ts | 27 ++ core/protocols/dashboard/msg-is.ts | 9 +- core/protocols/dashboard/msg-types.ts | 7 + dashboard/backend/api.ts | 247 +++------- dashboard/backend/bound-local-dbname.ts | 15 + dashboard/backend/cloud-token.ts | 445 ++++++++++++++++++ dashboard/backend/create-handler.ts | 6 +- dashboard/backend/db-api-schema.ts | 1 + dashboard/backend/db-api.test.ts | 260 +++++++++- dashboard/backend/device-id-svc.ts.off | 160 +++++++ dashboard/backend/tenants.ts | 8 +- pnpm-lock.yaml | 196 ++++++++ pnpm-workspace.yaml | 1 + use-fireproof/fp-cloud-connect-strategy.ts | 6 +- use-fireproof/index.ts | 1 + use-fireproof/package.json | 2 + 34 files changed, 1541 insertions(+), 296 deletions(-) create mode 100644 cloud/connector/base/convert-to-token-and-claims.ts rename {use-fireproof/fp-cloud-connector => cloud/connector/base}/fpcc-protocol.ts (100%) create mode 100644 cloud/connector/base/index.ts create mode 100644 cloud/connector/base/package.json rename {use-fireproof/fp-cloud-connector => cloud/connector/base}/post-messager.ts (100%) rename {use-fireproof/fp-cloud-connector => cloud/connector/base}/protocol-fp-cloud-conn.ts (100%) rename {use-fireproof/fp-cloud-connector => cloud/connector/iframe}/clerk-fpcc-evt-entity.ts (96%) rename {use-fireproof/fp-cloud-connector => cloud/connector/iframe}/fp-cloud-connector.ts (95%) rename {use-fireproof/fp-cloud-connector => cloud/connector/iframe}/iframe-fpcc-protocol.ts (81%) create mode 100644 cloud/connector/iframe/index.ts rename {use-fireproof/fp-cloud-connector => cloud/connector/iframe}/injected-iframe.html (100%) create mode 100644 cloud/connector/iframe/package.json create mode 100644 cloud/connector/page/index.ts create mode 100644 cloud/connector/page/package.json rename {use-fireproof/fp-cloud-connector => cloud/connector/page}/page-fpcc-protocol.ts (98%) rename {use-fireproof/fp-cloud-connector => cloud/connector/page}/page-handler.ts (97%) create mode 100644 cloud/connector/test/package.json rename {use-fireproof/fp-cloud-connector => cloud/connector/test}/page-fpcc-protocol.test.ts (60%) create mode 100644 cloud/connector/test/vitest.config.ts create mode 100644 dashboard/backend/bound-local-dbname.ts create mode 100644 dashboard/backend/cloud-token.ts create mode 100644 dashboard/backend/device-id-svc.ts.off diff --git a/cloud/connector/base/convert-to-token-and-claims.ts b/cloud/connector/base/convert-to-token-and-claims.ts new file mode 100644 index 000000000..00d4979d5 --- /dev/null +++ b/cloud/connector/base/convert-to-token-and-claims.ts @@ -0,0 +1,36 @@ +import { Logger, exception2Result, Result } from "@adviser/cement"; +import { FPCloudClaimSchema, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { jwtVerify } from "jose"; +import { JWKPublic } from "@fireproof/core-types-base"; + +export async function convertToTokenAndClaims( + dashApi: { + getClerkPublishableKey(): Promise<{ cloudPublicKeys: JWKPublic[] }>; + }, + logger: Logger, + token: string, +): Promise> { + for (const jwkPublic of await dashApi.getClerkPublishableKey().then((r) => r.cloudPublicKeys)) { + const rUnknownClaims = await exception2Result(() => jwtVerify(token, jwkPublic)); + if (rUnknownClaims.isErr() || !rUnknownClaims.Ok()?.payload) { + logger + .Warn() + .Err(rUnknownClaims) + .Any({ + kid: jwkPublic.kid, + }) + .Msg("Token failed"); + continue; + } + const rFPCloudClaim = FPCloudClaimSchema.safeParse(rUnknownClaims.Ok().payload); + if (!rFPCloudClaim.success) { + logger.Warn().Err(rFPCloudClaim.error).Msg("Token claims validation failed"); + continue; + } + return Result.Ok({ + token, + claims: rFPCloudClaim.data, + }); + } + return Result.Err("No valid JWK found to verify token"); +} diff --git a/use-fireproof/fp-cloud-connector/fpcc-protocol.ts b/cloud/connector/base/fpcc-protocol.ts similarity index 100% rename from use-fireproof/fp-cloud-connector/fpcc-protocol.ts rename to cloud/connector/base/fpcc-protocol.ts diff --git a/cloud/connector/base/index.ts b/cloud/connector/base/index.ts new file mode 100644 index 000000000..8e7acc53c --- /dev/null +++ b/cloud/connector/base/index.ts @@ -0,0 +1,13 @@ +export * from "./convert-to-token-and-claims.js"; +export * from "./fpcc-protocol.js"; +export * from "./post-messager.js"; +export * from "./protocol-fp-cloud-conn.js"; + +export interface DbKey { + readonly appId: string; + readonly dbName: string; +} + +export function dbAppKey(o: DbKey): string { + return o.appId + ":" + o.dbName; +} diff --git a/cloud/connector/base/package.json b/cloud/connector/base/package.json new file mode 100644 index 000000000..e8f6b6be6 --- /dev/null +++ b/cloud/connector/base/package.json @@ -0,0 +1,41 @@ +{ + "name": "@fireproof/cloud-connector-base", + "version": "0.0.0", + "description": "cloud connector shared", + "type": "module", + "scripts": { + "build": "core-cli tsc", + "pack": "core-cli build --doPack", + "publish": "core-cli build" + }, + "keywords": [ + "ledger", + "JSON", + "document", + "IPLD", + "CID", + "IPFS" + ], + "contributors": [ + "Meno Abels" + ], + "author": "Meno Abels", + "license": "AFL-2.0", + "homepage": "https://use-fireproof.com", + "repository": { + "type": "git", + "url": "git+https://github.com/fireproof-storage/fireproof.git" + }, + "bugs": { + "url": "https://github.com/fireproof-storage/fireproof/issues" + }, + "dependencies": { + "@adviser/cement": "^0.4.53", + "@fireproof/core-runtime": "workspace:*", + "@fireproof/core-types-base": "workspace:*", + "@fireproof/core-types-protocols-cloud": "workspace:*", + "jose": "^6.0.12", + "ts-essentials": "^10.1.1", + "zod": "^4.1.12" + } +} diff --git a/use-fireproof/fp-cloud-connector/post-messager.ts b/cloud/connector/base/post-messager.ts similarity index 100% rename from use-fireproof/fp-cloud-connector/post-messager.ts rename to cloud/connector/base/post-messager.ts diff --git a/use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts b/cloud/connector/base/protocol-fp-cloud-conn.ts similarity index 100% rename from use-fireproof/fp-cloud-connector/protocol-fp-cloud-conn.ts rename to cloud/connector/base/protocol-fp-cloud-conn.ts diff --git a/use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts similarity index 96% rename from use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts rename to cloud/connector/iframe/clerk-fpcc-evt-entity.ts index 038cc5f61..6493cd17c 100644 --- a/use-fireproof/fp-cloud-connector/clerk-fpcc-evt-entity.ts +++ b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts @@ -1,11 +1,11 @@ import { Lazy, Logger, poller, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { DashApi, AuthType, isResCloudDbTokenBound } from "@fireproof/core-protocols-dashboard"; -import { BackendFPCC, convertToTokenAndClaims, DbKey, GetCloudDbTokenResult } from "./iframe-fpcc-protocol.js"; +import { BackendFPCC, GetCloudDbTokenResult } from "./iframe-fpcc-protocol.js"; import { ensureLogger, exceptionWrapper, sleep } from "@fireproof/core-runtime"; -import { FPCCEvtApp, FPCCMsgBase } from "./protocol-fp-cloud-conn.js"; import { TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; -import { Clerk } from "@clerk/clerk-js"; +import { Clerk } from "@clerk/clerk-js/headless"; +import { DbKey, FPCCEvtApp, FPCCMsgBase, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; const clerkSvc = Lazy(async (dashApi: DashApi) => { const clerkPubKey = await dashApi.getClerkPublishableKey({}); diff --git a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts b/cloud/connector/iframe/fp-cloud-connector.ts similarity index 95% rename from use-fireproof/fp-cloud-connector/fp-cloud-connector.ts rename to cloud/connector/iframe/fp-cloud-connector.ts index 293a62054..a08153cf9 100644 --- a/use-fireproof/fp-cloud-connector/fp-cloud-connector.ts +++ b/cloud/connector/iframe/fp-cloud-connector.ts @@ -1,6 +1,6 @@ import { ensureSuperThis } from "@fireproof/core-runtime"; import { BuildURI, Lazy, URI } from "@adviser/cement"; -import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; +import { FPCCMessage } from "@fireproof/cloud-connector-base"; import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; export const fpCloudConnector = Lazy(async (loadUrlStr: string) => { diff --git a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts b/cloud/connector/iframe/iframe-fpcc-protocol.ts similarity index 81% rename from use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts rename to cloud/connector/iframe/iframe-fpcc-protocol.ts index 3cd4e5cee..391b74e85 100644 --- a/use-fireproof/fp-cloud-connector/iframe-fpcc-protocol.ts +++ b/cloud/connector/iframe/iframe-fpcc-protocol.ts @@ -1,6 +1,6 @@ import { ensureLogger, sleep } from "@fireproof/core-runtime"; -import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; import { + convertToTokenAndClaims, FPCCEvtApp, FPCCEvtConnectorReady, FPCCEvtNeedsLogin, @@ -10,9 +10,13 @@ import { FPCCSendMessage, isFPCCReqRegisterLocalDbName, isFPCCReqWaitConnectorReady, -} from "./protocol-fp-cloud-conn.js"; + FPCCProtocol, + FPCCProtocolBase, + dbAppKey, + DbKey, +} from "@fireproof/cloud-connector-base"; import { SuperThis } from "@fireproof/core-types-base"; -import { BuildURI, exception2Result, KeyedResolvSeq, Logger, Result } from "@adviser/cement"; +import { BuildURI, KeyedResolvSeq, Logger, Result } from "@adviser/cement"; import { DashApi, AuthType, @@ -20,9 +24,8 @@ import { ResCloudDbTokenNotBound, isResCloudDbTokenBound, } from "@fireproof/core-protocols-dashboard"; -import { FPCloudClaimSchema, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; import { ClerkFPCCEvtEntity } from "./clerk-fpcc-evt-entity.js"; -import { jwtVerify } from "jose"; export interface IframeFPCCProtocolOpts { readonly dashboardURI: string; @@ -30,15 +33,6 @@ export interface IframeFPCCProtocolOpts { // readonly backend: BackendFPCC; } -export interface DbKey { - readonly appId: string; - readonly dbName: string; -} - -export function dbAppKey(o: DbKey): string { - return o.appId + ":" + o.dbName; -} - export type GetCloudDbTokenResult = | { readonly res: ResCloudDbTokenBound; @@ -74,32 +68,6 @@ const registeredDbs = new Map(); // return clerkFPCCEvtEntities.get(key).once(() => new ClerkFPCCEvtEntity(sthis, dashApi, req, deviceId)); // } -export async function convertToTokenAndClaims(dashApi: DashApi, logger: Logger, token: string): Promise> { - for (const jwkPublic of await dashApi.getClerkPublishableKey().then((r) => r.cloudPublicKeys)) { - const rUnknownClaims = await exception2Result(() => jwtVerify(token, jwkPublic)); - if (rUnknownClaims.isErr() || !rUnknownClaims.Ok()?.payload) { - logger - .Warn() - .Err(rUnknownClaims) - .Any({ - kid: jwkPublic.kid, - }) - .Msg("Token failed"); - continue; - } - const rFPCloudClaim = FPCloudClaimSchema.safeParse(rUnknownClaims.Ok().payload); - if (!rFPCloudClaim.success) { - logger.Warn().Err(rFPCloudClaim.error).Msg("Token claims validation failed"); - continue; - } - return Result.Ok({ - token, - claims: rFPCloudClaim.data, - }); - } - return Result.Err("No valid JWK found to verify token"); -} - export class IframeFPCCProtocol implements FPCCProtocol { readonly sthis: SuperThis; readonly logger: Logger; @@ -167,21 +135,78 @@ export class IframeFPCCProtocol implements FPCCProtocol { this.logger.Error().Err(rAuthToken).Msg("Failed to obtain auth token after login"); return; } - return Promise.allSettled( - fpccEvtNeedsLogin.loadDbNames.map(async (dbInfo) => backend.getCloudDbToken(rAuthToken.Ok().token)), - ).then((results) => { - results.forEach((res) => { - if (res.status === "fulfilled") { - const fpccEvtApp = res.value; - backend.setFPCCEvtApp(fpccEvtApp); - this.sendMessage(fpccEvtApp, srcEvent); - backend.setState("ready"); - // this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); - } else { - this.logger.Error().Err(res.reason).Msg("Failed to obtain token for DB after login"); + return backend + .getCloudDbToken({ + type: "clerk", + token: rAuthToken.Ok().token, + }) + .then((rCloudToken) => { + if (rCloudToken.isErr()) { + throw this.logger + .Error() + .Err(rCloudToken) + .Any({ + appId: backend.appId, + dbName: backend.dbName, + }) + .Msg("Failed to obtain DB token after login") + .AsError(); } + const cloudToken = rCloudToken.Ok(); + switch (cloudToken.res.status) { + case "not-bound": + throw this.logger + .Error() + .Str("status", cloudToken.res.status) + .Any({ + appId: backend.appId, + dbName: backend.dbName, + }) + .Msg("DB is still not bound after login") + .AsError(); + } + return cloudToken.res.token; + }) + .then((cloudToken) => convertToTokenAndClaims(this.dashApi, this.logger, cloudToken)) + .then((rTanc) => { + if (rTanc.isErr()) { + throw this.logger + .Error() + .Err(rTanc) + .Any({ + appId: backend.appId, + dbName: backend.dbName, + }) + .Msg("Failed to convert DB token to token and claims after login"); + } + const { claims, token } = rTanc.Ok(); + const fpccEvtApp = { + tid: event.tid, + dst: event.src, + type: "FPCCEvtApp", + appId: backend.appId, + appFavIcon: { + defURL: "https://fireproof.direct/favicon.ico", + }, + devId: "", + user: { + name: claims.nickname ?? claims.userId, + email: claims.email, + provider: claims.provider ?? "unknown", + iconURL: "https://fireproof.direct/favicon.ico", + }, + localDb: { + dbName: backend.dbName, + tenantId: claims.selected.tenant, + ledgerId: claims.selected.ledger, + accessToken: token, + }, + env: {}, // future env vars + } satisfies FPCCSendMessage; + backend.setState("ready"); + backend.setFPCCEvtApp(this.sendMessage(fpccEvtApp, srcEvent)); + // this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); }); - }); }); } @@ -369,9 +394,9 @@ export class IframeFPCCProtocol implements FPCCProtocol { return this; } - sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): void { + sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): T { // message.src = window.location.href; // console.log("IframeFPCCProtocol sendMessage called", message); - this.fpccProtocol.sendMessage(message, srcEvent); + return this.fpccProtocol.sendMessage(message, srcEvent); } } diff --git a/cloud/connector/iframe/index.ts b/cloud/connector/iframe/index.ts new file mode 100644 index 000000000..40f74ed8e --- /dev/null +++ b/cloud/connector/iframe/index.ts @@ -0,0 +1,3 @@ +export * from "./clerk-fpcc-evt-entity.js"; +export * from "./fp-cloud-connector.js"; +export * from "./iframe-fpcc-protocol.js"; diff --git a/use-fireproof/fp-cloud-connector/injected-iframe.html b/cloud/connector/iframe/injected-iframe.html similarity index 100% rename from use-fireproof/fp-cloud-connector/injected-iframe.html rename to cloud/connector/iframe/injected-iframe.html diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json new file mode 100644 index 000000000..4ecf4e76c --- /dev/null +++ b/cloud/connector/iframe/package.json @@ -0,0 +1,42 @@ +{ + "name": "@fireproof/cloud-connector-iframe", + "version": "0.0.0", + "description": "cloud connector shared", + "type": "module", + "main": "./index.js", + "scripts": { + "build": "core-cli tsc", + "pack": "core-cli build --doPack", + "publish": "core-cli build" + }, + "keywords": [ + "ledger", + "JSON", + "document", + "IPLD", + "CID", + "IPFS" + ], + "contributors": [ + "Meno Abels" + ], + "author": "Meno Abels", + "license": "AFL-2.0", + "homepage": "https://use-fireproof.com", + "repository": { + "type": "git", + "url": "git+https://github.com/fireproof-storage/fireproof.git" + }, + "bugs": { + "url": "https://github.com/fireproof-storage/fireproof/issues" + }, + "dependencies": { + "@adviser/cement": "^0.4.53", + "@fireproof/core-runtime": "workspace:*", + "@fireproof/core-types-base": "workspace:*", + "@fireproof/core-protocols-dashboard": "workspace:*", + "@fireproof/core-types-protocols-cloud": "workspace:*", + "@fireproof/cloud-connector-base": "workspace:*", + "@clerk/clerk-js": "^5.102.0" + } +} diff --git a/cloud/connector/page/index.ts b/cloud/connector/page/index.ts new file mode 100644 index 000000000..ecd22de23 --- /dev/null +++ b/cloud/connector/page/index.ts @@ -0,0 +1,2 @@ +export * from "./page-fpcc-protocol.js"; +export * from "./page-handler.js"; diff --git a/cloud/connector/page/package.json b/cloud/connector/page/package.json new file mode 100644 index 000000000..a92c76d75 --- /dev/null +++ b/cloud/connector/page/package.json @@ -0,0 +1,40 @@ +{ + "name": "@fireproof/cloud-connector-page", + "version": "0.0.0", + "description": "cloud connector shared", + "type": "module", + "main": "./index.js", + "scripts": { + "build": "core-cli tsc", + "pack": "core-cli build --doPack", + "publish": "core-cli build" + }, + "keywords": [ + "ledger", + "JSON", + "document", + "IPLD", + "CID", + "IPFS" + ], + "contributors": [ + "Meno Abels" + ], + "author": "Meno Abels", + "license": "AFL-2.0", + "homepage": "https://use-fireproof.com", + "repository": { + "type": "git", + "url": "git+https://github.com/fireproof-storage/fireproof.git" + }, + "bugs": { + "url": "https://github.com/fireproof-storage/fireproof/issues" + }, + "dependencies": { + "@adviser/cement": "^0.4.53", + "@fireproof/cloud-connector-base": "workspace:*", + "@fireproof/core-types-base": "workspace:*", + "@fireproof/core-runtime": "workspace:*", + "ts-essentials": "^10.1.1" + } +} diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts b/cloud/connector/page/page-fpcc-protocol.ts similarity index 98% rename from use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts rename to cloud/connector/page/page-fpcc-protocol.ts index fc4cfd3df..03389dd4a 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.ts +++ b/cloud/connector/page/page-fpcc-protocol.ts @@ -1,8 +1,9 @@ import { ensureLogger, sleep } from "@fireproof/core-runtime"; -import { FPCCProtocol, FPCCProtocolBase } from "./fpcc-protocol.js"; import { SuperThis } from "@fireproof/core-types-base"; import { Future, KeyedResolvOnce, Logger, ResolveOnce, Result } from "@adviser/cement"; import { + FPCCProtocol, + FPCCProtocolBase, FPCCEvtApp, FPCCEvtNeedsLogin, FPCCMessage, @@ -13,9 +14,8 @@ import { isFPCCEvtApp, isFPCCEvtConnectorReady, isFPCCEvtNeedsLogin, -} from "./protocol-fp-cloud-conn.js"; - -import { dbAppKey } from "./iframe-fpcc-protocol.js"; + dbAppKey, +} from "@fireproof/cloud-connector-base"; export interface PageFPCCProtocolOpts { readonly maxConnectRetries?: number; diff --git a/use-fireproof/fp-cloud-connector/page-handler.ts b/cloud/connector/page/page-handler.ts similarity index 97% rename from use-fireproof/fp-cloud-connector/page-handler.ts rename to cloud/connector/page/page-handler.ts index e8a03effb..39709485a 100644 --- a/use-fireproof/fp-cloud-connector/page-handler.ts +++ b/cloud/connector/page/page-handler.ts @@ -3,9 +3,9 @@ */ import { Future } from "@adviser/cement"; -import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; -import { FPCCMessage } from "./protocol-fp-cloud-conn.js"; import { Writable } from "ts-essentials"; +import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; +import { FPCCMessage } from "@fireproof/cloud-connector-base"; /** * Creates an iframe element with the specified source diff --git a/cloud/connector/test/package.json b/cloud/connector/test/package.json new file mode 100644 index 000000000..d1103ce66 --- /dev/null +++ b/cloud/connector/test/package.json @@ -0,0 +1,42 @@ +{ + "name": "@fireproof/cloud-connector-test", + "version": "0.0.0", + "description": "cloud connector shared test", + "type": "module", + "private": "true", + "scripts": { + "build": "core-cli tsc", + "pack": "core-cli build --doPack", + "publish": "core-cli build", + "test": "vitest --run" + }, + "keywords": [ + "ledger", + "JSON", + "document", + "IPLD", + "CID", + "IPFS" + ], + "contributors": [ + "Meno Abels" + ], + "author": "Meno Abels", + "license": "AFL-2.0", + "homepage": "https://use-fireproof.com", + "repository": { + "type": "git", + "url": "git+https://github.com/fireproof-storage/fireproof.git" + }, + "bugs": { + "url": "https://github.com/fireproof-storage/fireproof/issues" + }, + "dependencies": { + "@fireproof/cloud-connector-base": "workspace:*", + "@fireproof/cloud-connector-iframe": "workspace:*", + "@fireproof/cloud-connector-page": "workspace:*", + "@fireproof/core-runtime": "workspace:*", + "@vitest/browser": "^3.2.4", + "ts-essentials": "^10.1.1" + } +} diff --git a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts b/cloud/connector/test/page-fpcc-protocol.test.ts similarity index 60% rename from use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts rename to cloud/connector/test/page-fpcc-protocol.test.ts index a043ae1cf..bf68fe245 100644 --- a/use-fireproof/fp-cloud-connector/page-fpcc-protocol.test.ts +++ b/cloud/connector/test/page-fpcc-protocol.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; -import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; -import { FPCCMessage, FPCCPing } from "./protocol-fp-cloud-conn.js"; +import { PageFPCCProtocol } from "@fireproof/cloud-connector-page"; +import { IframeFPCCProtocol } from "@fireproof/cloud-connector-iframe"; +import { FPCCMessage, FPCCPing } from "@fireproof/cloud-connector-base"; import { ensureSuperThis } from "@fireproof/core-runtime"; import { Writable } from "ts-essentials"; @@ -70,37 +70,37 @@ describe("FPCC Protocol", () => { pageProtocol.stop(); }); - it("registerApp", async () => { - await protocolStart(); - const fpccEvtApp = await pageProtocol.registerDatabase("wurst", { - tid: "tid-test-app-1", - appId: "test-app-1", - }); - expect(fpccEvtApp.Ok()).toEqual({ - tid: "tid-test-app-1", - type: "FPCCEvtApp", - src: "fp-cloud-connector", - dst: "page", - devId: "we-need-to-implement-device-id", - appId: "test-app-1", - appFavIcon: { - defURL: "https://example.com/favicon.ico", - }, - env: {}, - localDb: { - accessToken: expect.any(String), - // "auth-token-for-test-app-1-wurst-with-fake-auth-token:zMKseTNm6BhLCJNxy6AtXEe:https://dev.connect.fireproof.direct/api", - ledgerId: "ledger-for-test-app-1", - dbName: "wurst", - tenantId: "tenant-for-test-app-1", - }, - user: { - email: "test@example.com", - iconURL: "https://example.com/icon.png", - name: "Test User", - provider: "google", - }, - }); - pageProtocol.stop(); - }); + // it("registerApp", async () => { + // await protocolStart(); + // const fpccEvtApp = await pageProtocol.registerDatabase("wurst", { + // tid: "tid-test-app-1", + // appId: "test-app-1", + // }); + // expect(fpccEvtApp.Ok()).toEqual({ + // tid: "tid-test-app-1", + // type: "FPCCEvtApp", + // src: "fp-cloud-connector", + // dst: "page", + // devId: "we-need-to-implement-device-id", + // appId: "test-app-1", + // appFavIcon: { + // defURL: "https://example.com/favicon.ico", + // }, + // env: {}, + // localDb: { + // accessToken: expect.any(String), + // // "auth-token-for-test-app-1-wurst-with-fake-auth-token:zMKseTNm6BhLCJNxy6AtXEe:https://dev.connect.fireproof.direct/api", + // ledgerId: "ledger-for-test-app-1", + // dbName: "wurst", + // tenantId: "tenant-for-test-app-1", + // }, + // user: { + // email: "test@example.com", + // iconURL: "https://example.com/icon.png", + // name: "Test User", + // provider: "google", + // }, + // }); + // pageProtocol.stop(); + // }); }); diff --git a/cloud/connector/test/vitest.config.ts b/cloud/connector/test/vitest.config.ts new file mode 100644 index 000000000..6558607e3 --- /dev/null +++ b/cloud/connector/test/vitest.config.ts @@ -0,0 +1,27 @@ +/// +/// + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + name: "cloud:connector:test", + exclude: ["dist/**", "node_modules/**", "examples/**", "gateway/file"], + include: ["**/*test.?(c|m)[jt]s?(x)"], + browser: { + enabled: true, + headless: true, + provider: "playwright", + instances: [ + { + browser: "chromium", + context: { + recordVideo: undefined, + recordHar: undefined, + }, + }, + ], + screenshotFailures: false, + }, + }, +}); diff --git a/core/protocols/dashboard/msg-is.ts b/core/protocols/dashboard/msg-is.ts index 2334339c1..7894bf2cd 100644 --- a/core/protocols/dashboard/msg-is.ts +++ b/core/protocols/dashboard/msg-is.ts @@ -20,6 +20,7 @@ import { ReqExtendToken, ReqClerkPublishableKey, ResClerkPublishableKey, + ReqCloudDbToken, } from "./msg-types.js"; interface FPApiMsgInterface { @@ -34,7 +35,8 @@ interface FPApiMsgInterface { isEnsureUser(jso: unknown): jso is ReqEnsureUser; isListTenantsByUser(jso: unknown): jso is ReqListTenantsByUser; isUpdateUserTenant(jso: unknown): jso is ReqUpdateUserTenant; - isCloudSessionToken(jso: unknown): jso is ReqCloudSessionToken; + isReqCloudSessionToken(jso: unknown): jso is ReqCloudSessionToken; + isReqCloudDbToken(jso: unknown): jso is ReqCloudDbToken; isReqTokenByResultId(jso: unknown): jso is ReqTokenByResultId; isResTokenByResultId(jso: unknown): jso is ResTokenByResultId; isReqClerkPublishableKey(jso: unknown): jso is ReqClerkPublishableKey; @@ -95,7 +97,7 @@ export class FAPIMsgImpl implements FPApiMsgInterface { isDeleteLedger(jso: unknown): jso is ReqDeleteLedger { return hasType(jso, "reqDeleteLedger"); } - isCloudSessionToken(jso: unknown): jso is ReqCloudSessionToken { + isReqCloudSessionToken(jso: unknown): jso is ReqCloudSessionToken { return hasType(jso, "reqCloudSessionToken"); } isReqTokenByResultId(jso: unknown): jso is ReqTokenByResultId { @@ -113,4 +115,7 @@ export class FAPIMsgImpl implements FPApiMsgInterface { isResClerkPublishableKey(jso: unknown): jso is ResClerkPublishableKey { return hasType(jso, "resClerkPublishableKey"); } + isReqCloudDbToken(jso: unknown): jso is ReqCloudDbToken { + return hasType(jso, "reqCloudDbToken"); + } } diff --git a/core/protocols/dashboard/msg-types.ts b/core/protocols/dashboard/msg-types.ts index 5fa38c4c7..dadf7b37c 100644 --- a/core/protocols/dashboard/msg-types.ts +++ b/core/protocols/dashboard/msg-types.ts @@ -137,6 +137,7 @@ export interface FPApiParameters { export type InCreateTenantParams = { readonly name?: string; + readonly defaultTenant?: boolean; readonly ownerUserId: string; } & Partial; @@ -433,6 +434,12 @@ export interface ResCloudDbTokenNotBound { readonly type: "resCloudDbToken"; readonly status: "not-bound"; readonly reason: string; + // helps in the binding process + readonly ledgers: { + readonly ledgerId: string; + readonly tenantId: string; + readonly name: string; + }[]; } export type ResCloudDbToken = ResCloudDbTokenBound | ResCloudDbTokenNotBound; diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index 82fa093b6..e2ca42e08 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -55,6 +55,8 @@ import { FPApiParameters, ResClerkPublishableKey, ReqClerkPublishableKey, + ReqCloudDbToken, + ResCloudDbToken, } from "@fireproof/core-protocols-dashboard"; import { prepareInviteTicket, sqlInviteTickets, sqlToInviteTickets } from "./invites.js"; import { sqlLedgerUsers, sqlLedgers, sqlToLedgers } from "./ledgers.js"; @@ -67,6 +69,7 @@ import { Role, ReadWrite, toRole, toReadWrite, FPCloudClaim } from "@fireproof/c import { SuperThis } from "@fireproof/core-types-base"; import { sts } from "@fireproof/core-runtime"; import { DashSqlite } from "./create-handler.js"; +import { getCloudDbToken, getCloudSessionToken } from "./cloud-token.js"; function sqlToOutTenantParams(sql: typeof sqlTenants.$inferSelect): OutTenantParams { return { @@ -118,6 +121,8 @@ export interface FPApiInterface { // attachUserToLedger(req: ReqAttachUserToLedger): Promise getCloudSessionToken(req: ReqCloudSessionToken): Promise>; + + getCloudDbToken(req: ReqCloudDbToken): Promise>; getTokenByResultId(req: ReqTokenByResultId): Promise>; extendToken(req: ReqExtendToken): Promise>; } @@ -165,7 +170,7 @@ interface AddUserToTenant { readonly tenantName?: string; readonly tenantId: string; readonly userId: string; - readonly default?: boolean; + readonly defaultTenant?: boolean; readonly role: Role; readonly status?: UserStatus; readonly statusReason?: string; @@ -177,7 +182,7 @@ interface AddUserToLedger { readonly ledgerId: string; readonly tenantId: string; readonly userId: string; - readonly default?: boolean; + readonly defaultLedger?: boolean; readonly status?: UserStatus; readonly statusReason?: string; readonly role: Role; @@ -203,18 +208,28 @@ interface WithAuth { readonly auth: AuthType; } -interface ActiveUser { +export interface ActiveWithUser { + readonly verifiedAuth: T; + readonly user: User; +} + +export function isActiveWithUser(obj: ActivatedAuth): obj is ActiveWithUser { + return typeof obj === "object" && obj !== null && "verifiedAuth" in obj && "user" in obj; +} + +export interface ActiveVerified { readonly verifiedAuth: T; - readonly user?: User; } -type ActiveUserWithUserId = Omit, "user"> & { +type ActiveUserWithUserId = Omit, "user"> & { user: { userId: string; maxTenants: number; }; }; +type ActivatedAuth = ActiveWithUser | ActiveVerified; + function nameFromAuth(name: string | undefined, auth: ActiveUserWithUserId): string { return name ?? `${auth.verifiedAuth.params.email ?? nickFromClarkClaim(auth.verifiedAuth.params) ?? auth.verifiedAuth.userId}`; } @@ -235,6 +250,13 @@ export class FPApiSQL implements FPApiInterface { this.params = params; } + getCloudSessionToken(req: ReqCloudSessionToken, ictx: Partial = {}): Promise> { + return getCloudSessionToken(this, req, ictx); + } + getCloudDbToken(req: ReqCloudDbToken, ictx: Partial = {}): Promise> { + return getCloudDbToken(this, req, ictx); + } + private async _authVerifyAuth(req: { readonly auth: AuthType }): Promise> { // console.log("_authVerify-1", req); const tokenApi = this.tokenApi[req.auth.type]; @@ -254,7 +276,7 @@ export class FPApiSQL implements FPApiInterface { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - private async activeUser(req: WithAuth, status: UserStatus[] = ["active"]): Promise> { + async activeUser(req: WithAuth, status: UserStatus[] = ["active"]): Promise> { // console.log("activeUser-1", req); const rAuth = await this._authVerifyAuth(req); if (rAuth.isErr()) { @@ -283,9 +305,9 @@ export class FPApiSQL implements FPApiInterface { if (activeUser.isErr()) { return Result.Err(activeUser.Err()); } - const user = activeUser.Ok().user; - if (!user) { - const auth = activeUser.Ok().verifiedAuth; + const activated = activeUser.Ok(); + if (!isActiveWithUser(activated)) { + const auth = activated.verifiedAuth; const userId = this.sthis.nextId(12).str; const now = new Date(); await upsetUserByProvider( @@ -326,7 +348,7 @@ export class FPApiSQL implements FPApiInterface { tenantId: rTenant.Ok().tenantId, userId: userId, role: "admin", - default: true, + defaultTenant: true, }); // }); @@ -334,7 +356,7 @@ export class FPApiSQL implements FPApiInterface { } return Result.Ok({ type: "resEnsureUser", - user: user, + user: activated.user, tenants: await this.listTenantsByUser({ type: "reqListTenantsByUser", auth: req.auth, @@ -375,7 +397,7 @@ export class FPApiSQL implements FPApiInterface { tenantName: toUndef(tenant.name), tenantId: req.tenantId, userId: req.userId, - default: !!tenantUser.default, + defaultTenant: !!tenantUser.default, role: toRole(tenantUser.role), status: tenantUser.status as UserStatus, statusReason: tenantUser.statusReason, @@ -386,8 +408,8 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rCheck.Err()); } const now = new Date().toISOString(); - if (req.default) { - await db + if (req.defaultTenant) { + const x = await db .update(sqlTenantUsers) .set({ default: 0, @@ -395,7 +417,9 @@ export class FPApiSQL implements FPApiInterface { }) .where(and(eq(sqlTenantUsers.userId, req.userId), ne(sqlTenantUsers.default, 0))) .run(); + console.log("Clearing default tenant for user:", req.userId, req.tenantId, x); } + console.log("Adding user to tenant:", req.userId, "->", req.tenantId, "as", req.defaultTenant); const ret = ( await db .insert(sqlTenantUsers) @@ -404,18 +428,24 @@ export class FPApiSQL implements FPApiInterface { userId: req.userId, name: req.userName, role: req.role, - default: req.default ? 1 : 0, + default: req.defaultTenant ? 1 : 0, createdAt: now, updatedAt: now, }) .returning() )[0]; + const out = await db + .select() + .from(sqlTenantUsers) + .where(and(eq(sqlTenantUsers.userId, req.userId))) + .all(); + console.log("Added user to tenant:", out); return Result.Ok({ userName: toUndef(ret.name), tenantName: tenant.name, tenantId: tenant.tenantId, userId: ret.userId, - default: ret.default ? true : false, + defaultTenant: ret.default ? true : false, status: ret.status as UserStatus, statusReason: ret.statusReason, role: toRole(ret.role), @@ -490,7 +520,7 @@ export class FPApiSQL implements FPApiInterface { ledgerId: ledgerUser.Ledgers.ledgerId, tenantId: ledgerUser.Ledgers.tenantId, userId: req.userId, - default: !!ledgerUser.LedgerUsers.default, + defaultLedger: !!ledgerUser.LedgerUsers.default, status: ledgerUser.LedgerUsers.status as UserStatus, statusReason: ledgerUser.LedgerUsers.statusReason, role: toRole(ledgerUser.LedgerUsers.role), @@ -502,7 +532,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rCheck.Err()); } const now = new Date().toISOString(); - if (req.default) { + if (req.defaultLedger) { await db .update(sqlLedgerUsers) .set({ @@ -521,7 +551,7 @@ export class FPApiSQL implements FPApiInterface { name: req.userName, role: req.role, right: req.right, - default: req.default ? 1 : 0, + default: req.defaultLedger ? 1 : 0, createdAt: now, updatedAt: now, }) @@ -535,7 +565,7 @@ export class FPApiSQL implements FPApiInterface { status: ret.status as UserStatus, statusReason: ret.statusReason, userId: req.userId, - default: req.default ?? false, + defaultLedger: req.defaultLedger ?? false, role: toRole(ret.role), right: toReadWrite(ret.right), }); @@ -547,7 +577,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAUR.Err()); } const aur = rAUR.Ok(); - if (!aur.user) { + if (!isActiveWithUser(aur)) { return Result.Err(new UserNotFoundError()); } const tenantUsers = await this.db @@ -770,7 +800,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } return Result.Ok({ @@ -893,7 +923,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } const rRows = await queryUser(this.db, req.query); @@ -1043,7 +1073,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } const findUser = await queryUser(this.db, req.ticket.query); @@ -1170,7 +1200,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } let tenantCond = and(eq(sqlTenantUsers.userId, auth.user.userId), eq(sqlTenantUsers.status, "active")); @@ -1320,7 +1350,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } await this._deleteInvite(req.inviteId); @@ -1341,7 +1371,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } const userId = req.userId ?? auth.user.userId; @@ -1407,7 +1437,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } const rTenant = await this.insertTenant(auth as ActiveUserWithUserId, { @@ -1424,8 +1454,9 @@ export class FPApiSQL implements FPApiInterface { tenantId: tenant.tenantId, userId: auth.user.userId, role: "admin", - default: false, + defaultTenant: req.tenant.defaultTenant ?? false, }); + console.log(`Created tenant ${tenant.tenantId} for user ${auth.user.userId}: ${req.tenant.defaultTenant}`); return Result.Ok({ type: "resCreateTenant", tenant, @@ -1465,7 +1496,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } const prev = await this.db.select().from(sqlTenants).where(eq(sqlTenants.tenantId, req.tenant.tenantId)).get(); @@ -1499,7 +1530,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } // check if owner or admin of tenant @@ -1551,7 +1582,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } // check if owner or admin of tenant @@ -1611,7 +1642,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } const now = new Date().toISOString(); @@ -1703,7 +1734,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } // const now = new Date().toISOString(); @@ -1724,7 +1755,7 @@ export class FPApiSQL implements FPApiInterface { return Result.Err(rAuth.Err()); } const auth = rAuth.Ok(); - if (!auth.user) { + if (!isActiveWithUser(auth)) { return Result.Err(new UserNotFoundError()); } // const now = new Date().toISOString(); @@ -1745,145 +1776,6 @@ export class FPApiSQL implements FPApiInterface { }); } - async getCloudSessionToken(req: ReqCloudSessionToken, ictx: Partial = {}): Promise> { - const resListTenants = await this.listTenantsByUser({ - type: "reqListTenantsByUser", - auth: req.auth, - }); - if (resListTenants.isErr()) { - return Result.Err(resListTenants.Err()); - } - - const resListLedgers = await this.listLedgersByUser({ - type: "reqListLedgersByUser", - auth: req.auth, - }); - - if (resListLedgers.isErr()) { - return Result.Err(resListLedgers.Err()); - } - const rCtx = await getFPTokenContext(this.sthis, ictx); - if (rCtx.isErr()) { - return Result.Err(rCtx.Err()); - } - const ctx = rCtx.Ok(); - const rAuth = await this.activeUser(req); - if (rAuth.isErr()) { - return Result.Err(rAuth.Err()); - } - const auth = rAuth.Ok(); - if (!auth.user) { - return Result.Err(new UserNotFoundError()); - } - - // verify if tenant and ledger are valid - const selected = { - tenant: resListTenants.Ok().tenants[0]?.tenantId, - ledger: resListLedgers.Ok().ledgers[0]?.ledgerId, - }; - if ( - req.selected?.tenant && - resListTenants - .Ok() - .tenants.map((i) => i.tenantId) - .includes(req.selected?.tenant) - ) { - selected.tenant = req.selected?.tenant; - } - if ( - req.selected?.ledger && - resListLedgers - .Ok() - .ledgers.map((i) => i.ledgerId) - .includes(req.selected?.ledger) - ) { - selected.ledger = req.selected?.ledger; - } - const token = await createFPToken(ctx, { - userId: auth.user.userId, - tenants: resListTenants.Ok().tenants.map((i) => ({ - id: i.tenantId, - role: i.role, - })), - ledgers: resListLedgers - .Ok() - .ledgers.map((i) => { - const rights = i.users.find((u) => u.userId === auth.user?.userId); - if (!rights) { - return undefined; - } - return { - id: i.ledgerId, - role: rights.role, - right: rights.right, - }; - }) - .filter((i) => i) as FPCloudClaim["ledgers"], - email: auth.verifiedAuth.params.email, - nickname: auth.verifiedAuth.params.nick, - provider: toProvider(auth.verifiedAuth), - created: auth.user.createdAt, - selected: { - tenant: req.selected?.tenant ?? resListTenants.Ok().tenants[0]?.tenantId, - ledger: req.selected?.ledger ?? resListLedgers.Ok().ledgers[0]?.ledgerId, - }, - } satisfies FPCloudClaim); - - // console.log("getCloudSessionToken", { - // result: req.resultId, - // }); - if (req.resultId && req.resultId.length > "laenger".length) { - await this.addTokenByResultId({ - status: "found", - resultId: req.resultId, - token, - now: new Date(), - }); - // console.log("getCloudSessionToken-ok", { - // result: req.resultId, - // }); - } else if (req.resultId) { - this.sthis.logger.Warn().Any({ resultId: req.resultId }).Msg("resultId too short"); - console.log("getCloudSessionToken-failed", { - result: req.resultId, - }); - } - // console.log(">>>>-post:", ctx, privKey) - return Result.Ok({ - type: "resCloudSessionToken", - token, - }); - } - - async addTokenByResultId(req: TokenByResultIdParam): Promise> { - const now = (req.now ?? new Date()).toISOString(); - await this.db - .insert(sqlTokenByResultId) - .values({ - resultId: req.resultId, - status: req.status, - token: req.token, - createdAt: now, - updatedAt: now, - }) - .onConflictDoUpdate({ - target: [sqlTokenByResultId.resultId], - set: { - updatedAt: now, - resultId: req.resultId, - token: req.token, - status: req.status, - }, - }) - .run(); - const past = new Date(new Date(now).getTime() - 15 * 60 * 1000).toISOString(); - await this.db.delete(sqlTokenByResultId).where(lt(sqlTokenByResultId.updatedAt, past)).run(); - return Result.Ok({ - type: "resTokenByResultId", - ...req, - }); - } - async getClerkPublishableKey(_req: ReqClerkPublishableKey): Promise> { return Result.Ok({ type: "resClerkPublishableKey", @@ -1959,10 +1851,3 @@ export class FPApiSQL implements FPApiInterface { } } } - -function toProvider(i: ClerkVerifyAuth): FPCloudClaim["provider"] { - if (i.params.nick) { - return "github"; - } - return "google"; -} diff --git a/dashboard/backend/bound-local-dbname.ts b/dashboard/backend/bound-local-dbname.ts new file mode 100644 index 000000000..199ac88e6 --- /dev/null +++ b/dashboard/backend/bound-local-dbname.ts @@ -0,0 +1,15 @@ +import { sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core"; + +export const sqlBoundLocalDbnames = sqliteTable( + "BoundLocalDbnames", + { + appId: text().notNull(), + localDbName: text().notNull(), + tenantId: text().notNull(), + ledgerId: text().notNull(), + deviceIds: text().notNull(), // JSON stringified array of device IDs + createdAt: text().notNull(), + updatedAt: text().notNull(), + }, + (table) => [primaryKey({ columns: [table.appId, table.localDbName] })], +); diff --git a/dashboard/backend/cloud-token.ts b/dashboard/backend/cloud-token.ts new file mode 100644 index 000000000..c9dd632f6 --- /dev/null +++ b/dashboard/backend/cloud-token.ts @@ -0,0 +1,445 @@ +import { Logger, Result } from "@adviser/cement"; +import { + ClerkVerifyAuth, + ReqCloudDbToken, + ReqCloudSessionToken, + ResCloudDbToken, + ResCloudSessionToken, + ResTokenByResultId, +} from "@fireproof/core-protocols-dashboard"; +import { FPCloudClaim, toRole } from "@fireproof/core-types-protocols-cloud"; +import { lt, eq, and } from "drizzle-orm"; +import { FPTokenContext, getFPTokenContext, createFPToken } from "./create-fp-token.js"; +import { sqlTokenByResultId } from "./token-by-result-id.js"; +import { UserNotFoundError } from "./users.js"; +import { ActiveWithUser, FPApiSQL, isActiveWithUser, TokenByResultIdParam } from "./api.js"; +import { sqlLedgers, sqlLedgerUsers } from "./ledgers.js"; +import { sqlTenantUsers } from "./tenants.js"; +import { ensureLogger } from "@fireproof/core-runtime"; +import { sqlBoundLocalDbnames } from "./bound-local-dbname.js"; + +function toProvider(i: ClerkVerifyAuth): FPCloudClaim["provider"] { + if (i.params.nick) { + return "github"; + } + return "google"; +} + +async function upsertBoundLocalDbname( + api: FPApiSQL, + appId: string, + localDbName: string, + tenantId: string, + ledgerId: string, + deviceId: string, +): Promise> { + const now = new Date().toISOString(); + const results = await api.db + .insert(sqlBoundLocalDbnames) + .values({ + appId, + localDbName, + tenantId, + ledgerId, + deviceIds: JSON.stringify([deviceId]), + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [sqlBoundLocalDbnames.appId, sqlBoundLocalDbnames.localDbName], + set: { + updatedAt: now, + }, + }) + .returning(); + if (results.length !== 1) { + return Result.Err("Failed to upsert bound local dbname"); + } + const result = results[0]; + if (result.deviceIds) { + const deviceIds: string[] = JSON.parse(result.deviceIds ?? "[]"); + const uniqueDeviceIds = Array.from(new Set([...deviceIds, deviceId])).sort(); + if (uniqueDeviceIds.length !== deviceIds.length) { + await api.db + .update(sqlBoundLocalDbnames) + .set({ + deviceIds: JSON.stringify(uniqueDeviceIds), + updatedAt: now, + }) + .where(and(eq(sqlBoundLocalDbnames.appId, appId), eq(sqlBoundLocalDbnames.localDbName, localDbName))) + .run(); + } + } + return Result.Ok(undefined); +} + +async function createBoundToken( + req: { ledgerId: string; tenantId: string }, + api: FPApiSQL, + auth: ActiveWithUser, + logger: Logger, + ctx: FPTokenContext, +): Promise> { + const tandl = await api.db + .select() + .from(sqlLedgers) + .innerJoin(sqlTenantUsers, eq(sqlLedgers.tenantId, sqlTenantUsers.tenantId)) + .innerJoin(sqlLedgerUsers, eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId)) + .where(and(eq(sqlLedgers.ledgerId, req.ledgerId), eq(sqlLedgers.tenantId, req.tenantId))) + .get(); + if (tandl) { + return Result.Ok({ + type: "resCloudDbToken", + status: "bound", + token: await createFPToken(ctx, { + userId: auth.user.userId, + tenants: [{ id: req.tenantId, role: toRole(tandl.TenantUsers.role) }], + ledgers: [ + { + id: req.ledgerId, + role: toRole(tandl.LedgerUsers.role), + right: tandl.LedgerUsers.right as "read" | "write", + }, + ], + email: auth.verifiedAuth.params.email, + nickname: auth.verifiedAuth.params.nick, + provider: toProvider(auth.verifiedAuth), + created: auth.user.createdAt, + selected: { + tenant: req.tenantId, + ledger: req.ledgerId, + }, + } satisfies FPCloudClaim), + }); + } + return logger + .Error() + .Any({ ...req }) + .Msg("User has no access to tenant or ledger") + .ResultError(); +} + +export async function getCloudDbToken( + api: FPApiSQL, + req: ReqCloudDbToken, + ictx: Partial, +): Promise> { + const logger = ensureLogger(api.sthis, "getCloudDbToken", { + appId: req.appId, + deviceId: req.deviceId, + localDbName: req.localDbName, + }); + const rCtx = await getFPTokenContext(api.sthis, ictx); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + const rAuth = await api.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!isActiveWithUser(auth)) { + return Result.Err(new UserNotFoundError()); + } + if (req.tenantId && req.ledgerId) { + // check if user has access to tenant and ledger + return createBoundToken( + { + ledgerId: req.ledgerId, + tenantId: req.tenantId, + }, + api, + auth, + logger, + ctx, + ); + } + const binding = await api.db + .select() + .from(sqlBoundLocalDbnames) + .where(and(eq(sqlBoundLocalDbnames.appId, req.appId), eq(sqlBoundLocalDbnames.localDbName, req.localDbName))) + .get(); + if (binding) { + return createBoundToken( + { + ledgerId: binding.ledgerId, + tenantId: binding.tenantId, + }, + api, + auth, + logger, + ctx, + ); + } + + let tenantToCreateLedger: string; + console.log("No binding found for localDbName:", req.localDbName, "appId:", req.appId, "tenantId:", req.tenantId); + if (!req.tenantId) { + const rListLedgers = await api.listLedgersByUser({ + type: "reqListLedgersByUser", + auth: req.auth, + }); + if (rListLedgers.isErr()) { + return Result.Err(rListLedgers.Err()); + } + const ledgersPerTenant = rListLedgers.Ok().ledgers.filter((l) => l.name === req.localDbName); + console.log("Ledgers with names for user:", auth.user.userId, ledgersPerTenant); + if (ledgersPerTenant.length === 1) { + return createBoundToken( + { + ledgerId: ledgersPerTenant[0].ledgerId, + tenantId: ledgersPerTenant[0].tenantId, + }, + api, + auth, + logger, + ctx, + ); + } + if (ledgersPerTenant.length > 1) { + return Result.Ok({ + type: "resCloudDbToken", + status: "not-bound", + reason: "Multiple ledgers exist for user; with the same name existing", + ledgers: ledgersPerTenant.map((l) => ({ + ledgerId: l.ledgerId, + tenantId: l.tenantId, + name: l.name, + })), + }); + } + const rListTenants = await api.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: req.auth, + }); + if (rListTenants.isErr()) { + return Result.Err(rListTenants); + } + if (rListTenants.Ok().tenants.length === 0) { + return logger.Error().Any({ userId: auth.user.userId }).Msg("User has no tenants").ResultError(); + } + const defaultTenant = rListTenants.Ok().tenants.find((i) => i.default); + if (!defaultTenant) { + return logger.Error().Any({ userId: auth.user.userId }).Msg("User has no default tenant").ResultError(); + } + + console.log("Using default tenant for user:", auth.user.userId, "->", defaultTenant.tenantId, rListTenants.Ok().tenants); + tenantToCreateLedger = defaultTenant.tenantId; + + // get all ledgers for user + // checkIf localName is as LedgerName + // return unbound token + // getDefault tenant for user + } else { + tenantToCreateLedger = req.tenantId; + const rListLedgers = await api.listLedgersByUser({ + type: "reqListLedgersByUser", + auth: req.auth, + tenantIds: [req.tenantId], + }); + if (rListLedgers.isErr()) { + return Result.Err(rListLedgers.Err()); + } + const ledgersPerTenant = rListLedgers.Ok().ledgers.filter((l) => l.name); + if (ledgersPerTenant.length === 1) { + return createBoundToken( + { + ledgerId: ledgersPerTenant[0].ledgerId, + tenantId: ledgersPerTenant[0].tenantId, + }, + api, + auth, + logger, + ctx, + ); + } + if (ledgersPerTenant.length > 1) { + return Result.Ok({ + type: "resCloudDbToken", + status: "not-bound", + reason: "Multiple ledgers exist for user; with the same name existing", + ledgers: ledgersPerTenant.map((l) => ({ + ledgerId: l.ledgerId, + tenantId: l.tenantId, + name: l.name, + })), + }); + } + } + // create ledger with localDbName in tenant + const rCreateLedger = await api.createLedger({ + type: "reqCreateLedger", + auth: req.auth, + ledger: { + tenantId: tenantToCreateLedger, + name: req.localDbName, + }, + }); + if (rCreateLedger.isErr()) { + return Result.Err(rCreateLedger.Err()); + } + await upsertBoundLocalDbname( + api, + req.appId, + req.localDbName, + tenantToCreateLedger, + rCreateLedger.Ok().ledger.ledgerId, + req.deviceId, + ); + + // create bound localDbName token + return createBoundToken( + { + ledgerId: rCreateLedger.Ok().ledger.ledgerId, + tenantId: tenantToCreateLedger, + }, + api, + auth, + logger, + ctx, + ); +} + +export async function getCloudSessionToken( + api: FPApiSQL, + req: ReqCloudSessionToken, + ictx: Partial, +): Promise> { + const resListTenants = await api.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: req.auth, + }); + if (resListTenants.isErr()) { + return Result.Err(resListTenants.Err()); + } + + const resListLedgers = await api.listLedgersByUser({ + type: "reqListLedgersByUser", + auth: req.auth, + }); + + if (resListLedgers.isErr()) { + return Result.Err(resListLedgers.Err()); + } + const rCtx = await getFPTokenContext(api.sthis, ictx); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + const rAuth = await api.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!isActiveWithUser(auth)) { + return Result.Err(new UserNotFoundError()); + } + + // verify if tenant and ledger are valid + const selected = { + tenant: resListTenants.Ok().tenants[0]?.tenantId, + ledger: resListLedgers.Ok().ledgers[0]?.ledgerId, + }; + if ( + req.selected?.tenant && + resListTenants + .Ok() + .tenants.map((i) => i.tenantId) + .includes(req.selected?.tenant) + ) { + selected.tenant = req.selected?.tenant; + } + if ( + req.selected?.ledger && + resListLedgers + .Ok() + .ledgers.map((i) => i.ledgerId) + .includes(req.selected?.ledger) + ) { + selected.ledger = req.selected?.ledger; + } + const token = await createFPToken(ctx, { + userId: auth.user.userId, + tenants: resListTenants.Ok().tenants.map((i) => ({ + id: i.tenantId, + role: i.role, + })), + ledgers: resListLedgers + .Ok() + .ledgers.map((i) => { + const rights = i.users.find((u) => u.userId === auth.user?.userId); + if (!rights) { + return undefined; + } + return { + id: i.ledgerId, + role: rights.role, + right: rights.right, + }; + }) + .filter((i) => i) as FPCloudClaim["ledgers"], + email: auth.verifiedAuth.params.email, + nickname: auth.verifiedAuth.params.nick, + provider: toProvider(auth.verifiedAuth), + created: auth.user.createdAt, + selected: { + tenant: req.selected?.tenant ?? resListTenants.Ok().tenants[0]?.tenantId, + ledger: req.selected?.ledger ?? resListLedgers.Ok().ledgers[0]?.ledgerId, + }, + } satisfies FPCloudClaim); + + // console.log("getCloudSessionToken", { + // result: req.resultId, + // }); + if (req.resultId && req.resultId.length > "laenger".length) { + await addTokenByResultId(api, { + status: "found", + resultId: req.resultId, + token, + now: new Date(), + }); + // console.log("getCloudSessionToken-ok", { + // result: req.resultId, + // }); + } else if (req.resultId) { + api.sthis.logger.Warn().Any({ resultId: req.resultId }).Msg("resultId too short"); + console.log("getCloudSessionToken-failed", { + result: req.resultId, + }); + } + // console.log(">>>>-post:", ctx, privKey) + return Result.Ok({ + type: "resCloudSessionToken", + token, + }); +} + +async function addTokenByResultId(api: FPApiSQL, req: TokenByResultIdParam): Promise> { + const now = (req.now ?? new Date()).toISOString(); + await api.db + .insert(sqlTokenByResultId) + .values({ + resultId: req.resultId, + status: req.status, + token: req.token, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [sqlTokenByResultId.resultId], + set: { + updatedAt: now, + resultId: req.resultId, + token: req.token, + status: req.status, + }, + }) + .run(); + const past = new Date(new Date(now).getTime() - 15 * 60 * 1000).toISOString(); + await api.db.delete(sqlTokenByResultId).where(lt(sqlTokenByResultId.updatedAt, past)).run(); + return Result.Ok({ + type: "resTokenByResultId", + ...req, + }); +} diff --git a/dashboard/backend/create-handler.ts b/dashboard/backend/create-handler.ts index 35761b720..e0de5c4b0 100644 --- a/dashboard/backend/create-handler.ts +++ b/dashboard/backend/create-handler.ts @@ -302,7 +302,7 @@ export async function createHandler( res = fpApi.deleteLedger(jso); break; - case FPAPIMsg.isCloudSessionToken(jso): + case FPAPIMsg.isReqCloudSessionToken(jso): res = fpApi.getCloudSessionToken(jso); break; @@ -318,6 +318,10 @@ export async function createHandler( res = fpApi.getClerkPublishableKey(jso); break; + case FPAPIMsg.isReqCloudDbToken(jso): + res = fpApi.getCloudDbToken(jso); + break; + default: return new Response("Invalid request", { status: 400, headers: DefaultHttpHeaders() }); } diff --git a/dashboard/backend/db-api-schema.ts b/dashboard/backend/db-api-schema.ts index 4bf9ea82c..cf6b6c3a3 100644 --- a/dashboard/backend/db-api-schema.ts +++ b/dashboard/backend/db-api-schema.ts @@ -3,3 +3,4 @@ export * from "./tenants.js"; export * from "./ledgers.js"; export * from "./invites.js"; export * from "./token-by-result-id.js"; +export * from "./bound-local-dbname.js"; diff --git a/dashboard/backend/db-api.test.ts b/dashboard/backend/db-api.test.ts index 58c42f9f6..e064b71ae 100644 --- a/dashboard/backend/db-api.test.ts +++ b/dashboard/backend/db-api.test.ts @@ -7,12 +7,14 @@ // import { userRef } from "./db-api-schema"; import { Result } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core-types-base"; +import { JWKPublic, SuperThis } from "@fireproof/core-types-base"; import { createClient } from "@libsql/client/node"; import { type LibSQLDatabase, drizzle } from "drizzle-orm/libsql"; import { jwtVerify } from "jose/jwt/verify"; import { FPApiSQL, type FPApiToken } from "./api.js"; import { + isResCloudDbTokenBound, + ResClerkPublishableKey, type AdminTenant, type AuthType, type QueryUser, @@ -21,9 +23,11 @@ import { type VerifiedAuth, } from "@fireproof/core-protocols-dashboard"; import { queryEmail, queryNick } from "./sql-helper.js"; -import { ensureSuperThis, sts } from "@fireproof/core-runtime"; -import { describe, beforeAll, expect, it } from "vitest"; +import { ensureLogger, ensureSuperThis, sts } from "@fireproof/core-runtime"; +import { describe, beforeAll, expect, it, assert } from "vitest"; import { resWellKnownJwks } from "./well-known-jwks.js"; +import { convertToTokenAndClaims } from "use-fireproof"; +import { base58btc } from "multiformats/bases/base58"; // // import { eq } from 'drizzle-orm' // // import { drizzle } from 'drizzle-orm/libsql'; @@ -79,6 +83,7 @@ describe("db-api", () => { reqs: ReqEnsureUser; ress: ResEnsureUser; }[]; + const logger = ensureLogger(sthis, "dashboard-backend-db-api-test"); beforeAll(async () => { const client = createClient({ url: `file://${process.cwd()}/dist/sqlite.db` }); db = drizzle(client); @@ -1039,6 +1044,255 @@ describe("db-api", () => { expect(verifyExtended.payload.iss).toBe("TEST_I"); expect(verifyExtended.payload.aud).toBe("TEST_A"); }); + + // describe("getCloudDbToken", () => { + const ctx = { + secretToken: + "z33KxHvFS3jLz72v9DeyGBqo7H34SCC1RA5LvQFCyDiU4r4YBR4jEZxZwA9TqBgm6VB5QzwjrZJoVYkpmHgH7kKJ6Sasat3jTDaBCkqWWfJAVrBL7XapUstnKW3AEaJJKvAYWrKYF9JGqrHNU8WVjsj3MZNyqqk8iAtTPPoKtPTLo2c657daVMkxibmvtz2egnK5wPeYEUtkbydrtBzteN25U7zmGqhS4BUzLjDiYKMLP8Tayi", + publicToken: + "zeWndr5LEoaySgKSo2aZniYqcrEJBPswFRe3bwyxY7Nmr3bznXkHhFm77VxHprvCskpKVHEwVzgQpM6SAYkUZpZcEdEunwKmLUYd1yJ4SSteExyZw4GC1SvJPLDpGxKBKb6jkkCsaQ3MJ5YFMKuGUkqpKH31Dw7cFfjdQr5XUiXue", + issuer: "TEST_I", + audience: "TEST_A", + validFor: 3600000, // 1 hour + }; + // eslint-disable-next-line no-restricted-globals + const publicKey = JSON.parse(new TextDecoder().decode(base58btc.decode(ctx.publicToken))) as JWKPublic; + + it("with ledger and tenant ", async () => { + const tenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: { + // ownerUserId: data[0].ress.user.userId, + }, + }); + const ledger = await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: tenant.Ok().tenant.tenantId, + name: `DB Token Ledger`, + }, + }); + const rRes = await fpApi.getCloudDbToken( + { + type: "reqCloudDbToken", + auth: data[0].reqs.auth, + tenantId: tenant.Ok().tenant.tenantId, + ledgerId: ledger.Ok().ledger.ledgerId, + localDbName: "not-existing-db", + appId: "not-existing-app", + deviceId: "not-existing-device", + }, + ctx, + ); + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + assert.fail("Expected not bound response"); + return; + } + const rTandC = await convertToTokenAndClaims( + { + getClerkPublishableKey(): Promise { + return Promise.resolve({ + type: "resClerkPublishableKey", + publishableKey: "undefined", + cloudPublicKeys: [publicKey], + }); + }, + }, + logger, + res.token, + ); + // console.log(rTandC); + expect(rTandC.isOk()).toBeTruthy(); + const tandC = rTandC.Ok(); + expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); + expect(tandC.claims.selected.ledger).toBe(ledger.Ok().ledger.ledgerId); + expect(tandC.claims.ledgers).toEqual([ + { + id: ledger.Ok().ledger.ledgerId, + right: "write", + role: "admin", + }, + ]); + }); + it("with non existing ledger and tenant", async () => { + const rRes = await fpApi.getCloudDbToken( + { + type: "reqCloudDbToken", + auth: data[0].reqs.auth, + ledgerId: "non-existing-ledger", + tenantId: "non-existing-tenant", + localDbName: `no-existing-local-db-${sthis.nextId(6).str}`, + appId: "not-existing-app", + deviceId: "not-existing-device", + }, + ctx, + ); + const res = rRes.Err(); + expect(JSON.parse(res.message).msg).toEqual("User has no access to tenant or ledger"); + }); + it("without ledger and tenant but appId and localDbName not existing no ambiguity", async () => { + const tenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: { + defaultTenant: true, + // ownerUserId: data[0].ress.user.userId, + }, + }); + const rRes = await fpApi.getCloudDbToken( + { + type: "reqCloudDbToken", + auth: data[0].reqs.auth, + localDbName: `no-existing-local-db-${sthis.nextId(6).str}`, + appId: "not-existing-app", + deviceId: "not-existing-device", + }, + ctx, + ); + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + assert.fail("Expected not bound response"); + return; + } + const rTandC = await convertToTokenAndClaims( + { + getClerkPublishableKey(): Promise { + return Promise.resolve({ + type: "resClerkPublishableKey", + publishableKey: "undefined", + cloudPublicKeys: [publicKey], + }); + }, + }, + logger, + res.token, + ); + const tandC = rTandC.Ok(); + // console.log(data.map((i) => i.ress.tenants).map((j) => j.map((k) => k.tenantId))); + expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); + expect(tandC.claims.selected.ledger).toBeDefined(); + }); + it("without ledger and tenant but appId and localDbName existing", async () => { + const tenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: { + defaultTenant: true, + // ownerUserId: data[0].ress.user.userId, + }, + }); + const ledger = await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: tenant.Ok().tenant.tenantId, + name: `DB Token Ledger-${sthis.nextId(6).str}`, + }, + }); + const rRes = await fpApi.getCloudDbToken( + { + type: "reqCloudDbToken", + auth: data[0].reqs.auth, + localDbName: ledger.Ok().ledger.name, + appId: "not-existing-app", + deviceId: "not-existing-device", + }, + ctx, + ); + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + assert.fail("Expected not bound response"); + return; + } + const rTandC = await convertToTokenAndClaims( + { + getClerkPublishableKey(): Promise { + return Promise.resolve({ + type: "resClerkPublishableKey", + publishableKey: "undefined", + cloudPublicKeys: [publicKey], + }); + }, + }, + logger, + res.token, + ); + const tandC = rTandC.Ok(); + // console.log(data.map((i) => i.ress.tenants).map((j) => j.map((k) => k.tenantId))); + expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); + expect(tandC.claims.selected.ledger).toBe(ledger.Ok().ledger.ledgerId); + }); + it("without ledger and tenant but appId and localDbName and ambiguity", async () => { + const tenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: { + defaultTenant: true, + // ownerUserId: data[0].ress.user.userId, + }, + }); + const name = `DB Token Ledger-${sthis.nextId(6).str}`; + await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: tenant.Ok().tenant.tenantId, + name, + }, + }); + await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: tenant.Ok().tenant.tenantId, + name, + }, + }); + + const rRes = await fpApi.getCloudDbToken( + { + type: "reqCloudDbToken", + auth: data[0].reqs.auth, + tenantId: tenant.Ok().tenant.tenantId, + localDbName: name, + appId: "not-existing-app", + deviceId: "not-existing-device", + }, + ctx, + ); + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + assert.fail("Expected not bound response"); + return; + } + const rTandC = await convertToTokenAndClaims( + { + getClerkPublishableKey(): Promise { + return Promise.resolve({ + type: "resClerkPublishableKey", + publishableKey: "undefined", + cloudPublicKeys: [publicKey], + }); + }, + }, + logger, + res.token, + ); + const tandC = rTandC.Ok(); + // console.log(data.map((i) => i.ress.tenants).map((j) => j.map((k) => k.tenantId))); + expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); + expect(tandC.claims.selected.ledger).toBe("xxx"); //ledger.Ok().ledger.ledgerId); + }); + it("without ledger but tenant appId and localDbName and no ambiguity", async () => { + /* empty */ + }); + it("without ledger but tenant appId and localDbName and ambiguity", async () => { + /* empty */ + }); + // }); }); it("queryEmail strips +....@", async () => { diff --git a/dashboard/backend/device-id-svc.ts.off b/dashboard/backend/device-id-svc.ts.off new file mode 100644 index 000000000..9daa7ec74 --- /dev/null +++ b/dashboard/backend/device-id-svc.ts.off @@ -0,0 +1,160 @@ +import { SuperThis } from "@fireproof/core-types-base"; +import { DeviceIdProtocol, DeviceIdProtocolSrv, DeviceIdProtocolSrvOpts, GenerateSerialNumber } from "@fireproof/core-device-id"; +import { Result } from "@adviser/cement"; +import { calculateJwkThumbprint } from "jose"; +import { ClerkVerifyAuth, ReqEnsureDeviceId, ResEnsureDeviceId } from "@fireproof/core-protocols-dashboard"; +import { ActiveUser } from "./api.js"; +import { DashSqlite } from "./create-handler.js"; +import { sqlDevIdsPerUser } from "./device-ids.js"; +import { and, eq, gt, gte } from "drizzle-orm/sql/expressions"; + +export interface DeviceIdSvc extends Omit { + readonly db: DashSqlite; + readonly reAuthTime?: number; // seconds typically 30 days + readonly maxDevicesPerUser?: number; // typically 10 + readonly revalidateTime?: number; // seconds, typically 3 days +} + +export class DeviceIdService { + static async create(sthis: SuperThis, opts: DeviceIdSvc): Promise> { + const svc = new DeviceIdService(sthis, { + db: opts.db, + reAuthTime: opts.reAuthTime || (365 / 2) * 24 * 60 * 60, + maxDevicesPerUser: opts.maxDevicesPerUser || 10, + revalidateTime: opts.revalidateTime || ~~((opts.reAuthTime || (365 / 2) * 24 * 60 * 60) / 100), + }); + const res = await DeviceIdProtocolSrv.create(sthis, { + ...opts, + // actions: { + // generateSerialNumber: svc.generateSerialNumber.bind(svc), + // }, + }); + if (res.isErr()) { + return Result.Err(res); + } + return Result.Ok(svc); + } + + readonly sthis: SuperThis; + deviceIdProtocol!: DeviceIdProtocol; + readonly reAuthTime: number; + readonly revalidateTime: number; // window + readonly maxDevicesPerUser: number; + + constructor( + sthis: SuperThis, + readonly db: DashSqlite, + opts: DeviceIdSvc, + ) { + this.sthis = sthis; + this.db = db; + this.reAuthTime = opts.reAuthTime || (365 / 2) * 24 * 60 * 60; + this.maxDevicesPerUser = opts.maxDevicesPerUser || 10; + this.revalidateTime = opts.revalidateTime || ~~(this.reAuthTime / 100); + } + + isValidToAddDevice(userId: string): Promise> { + if (getResult.status !== "active") { + return Result.Err(`Device ID not active: ${getResult.status}:${getResult.deviceId}:${getResult.userId}`); + } + const allDevs = await this.db + .select() + .from(sqlDevIdsPerUser) + .where( + and( + eq(sqlDevIdsPerUser.userId, auth.user.userId), + gte(sqlDevIdsPerUser.updatedAt, new Date(Date.now() - this.reAuthTime * 1000).toISOString()), + ), + ) + .all(); + if (!allDevs || allDevs.length === 0) { + return Result.Err("No active device IDs found for user"); + } + const active = allDevs.filter((d) => d.status === "active"); + if (active.length !== allDevs.length) { + return Result.Err(`UserID is not active: ${auth.user.userId}`); + } + if (allDevs.length >= this.maxDevicesPerUser) { + return Result.Err(`Too many device IDs for user: ${allDevs.length} > ${this.maxDevicesPerUser}`); + } + } + + async ensureDeviceId(auth: ActiveUser, req: ReqEnsureDeviceId): Promise> { + const res = await this.deviceIdProtocol.issueCertificate(req.csrJWT, { + generateSerialNumber: async (pub, opts) => { + if (!auth.user) { + return Result.Err("User not authenticated"); + } + const hash = await calculateJwkThumbprint(pub); + const getResult = await this.db + .select() + .from(sqlDevIdsPerUser) + .where( + and( + eq(sqlDevIdsPerUser.userId, auth.user.userId), + eq(sqlDevIdsPerUser.deviceId, hash), + gt(sqlDevIdsPerUser.updatedAt, new Date(Date.now() - this.reAuthTime * 1000).toISOString()), + ), + ) + .get(); + if (!getResult) { + const isAddable = await this.isValidToAddDevice(auth.user.userId); + if (isAddable.isErr()) { + return Result.Err(isAddable.Err()); + } + const now = new Date().toISOString(); + const val = await this.db + .insert(sqlDevIdsPerUser) + .values({ + deviceId: hash, + userId: auth.user.userId, + status: "active", + serial: 1, + createdAt: now, + updatedAt: now, + }) + .returning() + .get(); + return Result.Ok({ + serialNr: val.serial, + issuedAt: new Date(val.updatedAt), + expiryAt: new Date(val.updatedAt + opts.validityPeriod * 1000), // default 1 year + } satisfies GenerateSerialNumber); + } + if (getResult.status !== "active") { + return Result.Err(`Device ID not active: ${getResult.status}:${getResult.deviceId}:${getResult.userId}`); + } + if (new Date(getResult.updatedAt) < new Date(Date.now() - opts.validityPeriod * 1000)) { + return Result.Ok({ + reauth: true, + }); + } + if (new Date(getResult.updatedAt) < new Date(Date.now() + (opts.validityPeriod - opts.revalidateTime) * 1000)) { + const now = new Date(); + await this.db + .update(sqlDevIdsPerUser) + .set({ + updatedAt: now.toISOString(), + serial: getResult.serial + 1, + }) + .where(and(eq(sqlDevIdsPerUser.deviceId, getResult.deviceId), eq(sqlDevIdsPerUser.userId, getResult.userId))) + .run(); + return Result.Ok({ + ...getResult, + serialNr: getResult.serial + 1, + status: "reActivated", + issuedAt: now, + } satisfies GenerateSerialNumber); + } + }, + }); + if (res.isErr()) { + return Result.Err(res); + } + + // .update(sqlInviteTickets) + // .set({ status: "expired" }) + // .where(and(eq(sqlInviteTickets.status, "pending"), lt(sqlInviteTickets.expiresAfter, new Date().toISOString()))) + // .run(); + } +} diff --git a/dashboard/backend/tenants.ts b/dashboard/backend/tenants.ts index 5e63d70c3..a8dfb7865 100644 --- a/dashboard/backend/tenants.ts +++ b/dashboard/backend/tenants.ts @@ -7,10 +7,10 @@ export const sqlTenants = sqliteTable("Tenants", { ownerUserId: text() .notNull() .references(() => sqlUsers.userId), - maxAdminUsers: int().notNull(), //.default(5), - maxMemberUsers: int().notNull(), //.default(5), - maxInvites: int().notNull(), // .default(10), - maxLedgers: int().notNull(), // .default(5), + maxAdminUsers: int().notNull().default(5), + maxMemberUsers: int().notNull().default(5), + maxInvites: int().notNull().default(10), + maxLedgers: int().notNull().default(5), status: text().notNull().default("active"), statusReason: text().notNull().default("just created"), createdAt: text().notNull(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 926db8d89..7674f8419 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,93 @@ importers: specifier: ^7.1.12 version: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + cloud/connector/base: + dependencies: + '@adviser/cement': + specifier: ^0.4.53 + version: 0.4.63(typescript@5.9.3) + '@fireproof/core-runtime': + specifier: workspace:* + version: link:../../../core/runtime + '@fireproof/core-types-base': + specifier: workspace:* + version: link:../../../core/types/base + '@fireproof/core-types-protocols-cloud': + specifier: workspace:* + version: link:../../../core/types/protocols/cloud + jose: + specifier: ^6.0.12 + version: 6.1.1 + ts-essentials: + specifier: ^10.1.1 + version: 10.1.1(typescript@5.9.3) + zod: + specifier: ^4.1.12 + version: 4.1.12 + + cloud/connector/iframe: + dependencies: + '@adviser/cement': + specifier: ^0.4.53 + version: 0.4.63(typescript@5.9.3) + '@clerk/clerk-js': + specifier: ^5.102.0 + version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) + '@fireproof/cloud-connector-base': + specifier: workspace:* + version: link:../base + '@fireproof/core-protocols-dashboard': + specifier: workspace:* + version: link:../../../core/protocols/dashboard + '@fireproof/core-runtime': + specifier: workspace:* + version: link:../../../core/runtime + '@fireproof/core-types-base': + specifier: workspace:* + version: link:../../../core/types/base + '@fireproof/core-types-protocols-cloud': + specifier: workspace:* + version: link:../../../core/types/protocols/cloud + + cloud/connector/page: + dependencies: + '@adviser/cement': + specifier: ^0.4.53 + version: 0.4.63(typescript@5.9.3) + '@fireproof/cloud-connector-base': + specifier: workspace:* + version: link:../base + '@fireproof/core-runtime': + specifier: workspace:* + version: link:../../../core/runtime + '@fireproof/core-types-base': + specifier: workspace:* + version: link:../../../core/types/base + ts-essentials: + specifier: ^10.1.1 + version: 10.1.1(typescript@5.9.3) + + cloud/connector/test: + dependencies: + '@fireproof/cloud-connector-base': + specifier: workspace:* + version: link:../base + '@fireproof/cloud-connector-iframe': + specifier: workspace:* + version: link:../iframe + '@fireproof/cloud-connector-page': + specifier: workspace:* + version: link:../page + '@fireproof/core-runtime': + specifier: workspace:* + version: link:../../../core/runtime + '@vitest/browser': + specifier: ^3.2.4 + version: 3.2.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8) + ts-essentials: + specifier: ^10.1.1 + version: 10.1.1(typescript@5.9.3) + cloud/todo-app: dependencies: '@adviser/cement': @@ -1269,6 +1356,12 @@ importers: '@adviser/cement': specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) + '@fireproof/cloud-connector-base': + specifier: workspace:* + version: link:../cloud/connector/base + '@fireproof/cloud-connector-page': + specifier: workspace:* + version: link:../cloud/connector/page '@fireproof/core-base': specifier: workspace:0.0.0 version: link:../core/base @@ -3157,6 +3250,12 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tsd/typescript@5.9.2': resolution: {integrity: sha512-mSMM0QtEPdMd+rdMDd17yCUYD4yI3pKHap89+jEZrZ3KIO5PhDofBjER0OtgHdvOXF74KMLO3fyD6k3Hz0v03A==} engines: {node: '>=14.17'} @@ -3351,6 +3450,21 @@ packages: playwright: '*' vitest: 4.0.8 + '@vitest/browser@3.2.4': + resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} + peerDependencies: + playwright: '*' + safaridriver: '*' + vitest: 3.2.4 + webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + '@vitest/browser@4.0.8': resolution: {integrity: sha512-oG6QJAR0d7S5SDnIYZwjxCj/a5fhbp9ZE7GtMgZn+yCUf4CxtqbBV6aXyg0qmn8nbUWT+rGuXL2ZB6qDBUjv/A==} peerDependencies: @@ -3359,6 +3473,17 @@ packages: '@vitest/expect@4.0.8': resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.0.8': resolution: {integrity: sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==} peerDependencies: @@ -3370,6 +3495,9 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.0.8': resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==} @@ -3379,9 +3507,15 @@ packages: '@vitest/snapshot@4.0.8': resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.0.8': resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.0.8': resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} @@ -4799,6 +4933,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5666,10 +5803,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -7613,6 +7758,10 @@ snapshots: '@types/react': 19.2.3 '@types/react-dom': 19.2.3(@types/react@19.2.3) + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tsd/typescript@5.9.2': {} '@types/aria-query@5.0.4': {} @@ -7854,6 +8003,25 @@ snapshots: - utf-8-validate - vite + '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/utils': 3.2.4 + magic-string: 0.30.21 + sirv: 3.0.2 + tinyrainbow: 2.0.0 + vitest: 4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + ws: 8.18.3 + optionalDependencies: + playwright: 1.56.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8)': dependencies: '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) @@ -7880,6 +8048,14 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 + '@vitest/mocker@3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.8 @@ -7888,6 +8064,10 @@ snapshots: optionalDependencies: vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/pretty-format@4.0.8': dependencies: tinyrainbow: 3.0.3 @@ -7903,8 +8083,18 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@4.0.8': {} + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@4.0.8': dependencies: '@vitest/pretty-format': 4.0.8 @@ -9500,6 +9690,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -10470,8 +10662,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@2.0.0: {} + tinyrainbow@3.0.3: {} + tinyspy@4.0.4: {} + tmp@0.2.5: {} to-regex-range@5.0.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c43359386..6b6c60735 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - "core/*" - "cloud/*" - "cloud/backend/*" + - "cloud/connector/*" - "cli" - "core/types/*" - "core/types/protocols/*" diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index 2df91874c..ba9aab3a4 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -4,12 +4,10 @@ import { ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-typ import { ensureLogger, ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; +import { PageFPCCProtocol, initializeIframe } from "@fireproof/cloud-connector-page"; -import { initializeIframe } from "./fp-cloud-connector/page-handler.js"; -import { PageFPCCProtocol } from "./fp-cloud-connector/page-fpcc-protocol.js"; -import { FPCCEvtApp, FPCCEvtNeedsLogin } from "./fp-cloud-connector/protocol-fp-cloud-conn.js"; +import { FPCCEvtApp, FPCCEvtNeedsLogin, dbAppKey } from "@fireproof/cloud-connector-base"; import DOMPurify from "dompurify"; -import { dbAppKey } from "./fp-cloud-connector/iframe-fpcc-protocol.js"; export interface FPCloudConnectOpts extends RedirectStrategyOpts { readonly dashboardURI?: string; diff --git a/use-fireproof/index.ts b/use-fireproof/index.ts index 9522d397f..478bf5f5d 100644 --- a/use-fireproof/index.ts +++ b/use-fireproof/index.ts @@ -20,6 +20,7 @@ import { toCloud as toCloudCore } from "@fireproof/core-gateways-cloud"; import { ensureSuperThis } from "@fireproof/core-runtime"; export { FPCloudConnectStrategy } from "./fp-cloud-connect-strategy.js"; +export { convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; export type UseFpToCloudParam = Omit, "context">, "events"> & Partial & { diff --git a/use-fireproof/package.json b/use-fireproof/package.json index 94ebd7631..9f0c44106 100644 --- a/use-fireproof/package.json +++ b/use-fireproof/package.json @@ -31,6 +31,8 @@ "@fireproof/core-types-base": "workspace:0.0.0", "@fireproof/core-types-blockstore": "workspace:0.0.0", "@fireproof/core-types-protocols-cloud": "workspace:0.0.0", + "@fireproof/cloud-connector-base": "workspace:*", + "@fireproof/cloud-connector-page": "workspace:*", "@fireproof/vendor": "workspace:0.0.0", "dompurify": "^3.3.0", "jose": "^6.1.1", From 0e6f210d8f812490afc4398ce16e249f4e9f4e2f Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 12:30:02 +0100 Subject: [PATCH 09/23] chore: lets test the deployment --- cloud/3rd-party/src/overlayHtml.tsx | 2 +- .../base/convert-to-token-and-claims.ts | 8 ++- cloud/connector/base/tsconfig.json | 6 ++ .../connector/iframe/clerk-fpcc-evt-entity.ts | 14 +++-- .../connector/iframe/iframe-fpcc-protocol.ts | 6 +- cloud/connector/iframe/index.ts | 2 + cloud/connector/iframe/injected-iframe.html | 13 +---- cloud/connector/iframe/injected-iframe.tsx | 47 +++++++++++++++ cloud/connector/iframe/package.json | 8 ++- cloud/connector/iframe/tsconfig.json | 6 ++ cloud/connector/page/tsconfig.json | 6 ++ cloud/connector/test/package.json | 4 +- cloud/connector/test/tsconfig.json | 6 ++ core/gateways/cloud/to-cloud.ts | 6 +- core/types/protocols/cloud/gateway-control.ts | 21 +++++-- dashboard/backend/cf-serve.ts | 20 +++++++ dashboard/backend/cloud-token.ts | 1 + dashboard/backend/db-api.test.ts | 14 ++--- dashboard/vite.config.ts | 58 ++++++++++++++++--- patches/@clerk__clerk-js.patch | 21 +++++++ pnpm-lock.yaml | 6 ++ use-fireproof/fp-cloud-connect-strategy.ts | 41 +++++++++---- use-fireproof/iframe-strategy.ts | 6 +- use-fireproof/index.ts | 4 +- use-fireproof/react/types.ts | 10 ++-- use-fireproof/react/use-attach.ts | 12 ++-- use-fireproof/redirect-strategy.ts | 17 ++++-- vitest.config.ts | 1 + 28 files changed, 283 insertions(+), 83 deletions(-) create mode 100644 cloud/connector/base/tsconfig.json create mode 100644 cloud/connector/iframe/injected-iframe.tsx create mode 100644 cloud/connector/iframe/tsconfig.json create mode 100644 cloud/connector/page/tsconfig.json create mode 100644 cloud/connector/test/tsconfig.json create mode 100644 patches/@clerk__clerk-js.patch diff --git a/cloud/3rd-party/src/overlayHtml.tsx b/cloud/3rd-party/src/overlayHtml.tsx index e7c33fade..502757c8e 100644 --- a/cloud/3rd-party/src/overlayHtml.tsx +++ b/cloud/3rd-party/src/overlayHtml.tsx @@ -4,7 +4,7 @@ import { createElement } from "preact"; const React = { createElement, }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars + // function jsxDEV(...args: unknown[]) { // // eslint-disable-next-line @typescript-eslint/no-explicit-any // return (React as any).call(React, ...args) diff --git a/cloud/connector/base/convert-to-token-and-claims.ts b/cloud/connector/base/convert-to-token-and-claims.ts index 00d4979d5..98f0ed990 100644 --- a/cloud/connector/base/convert-to-token-and-claims.ts +++ b/cloud/connector/base/convert-to-token-and-claims.ts @@ -1,8 +1,12 @@ import { Logger, exception2Result, Result } from "@adviser/cement"; -import { FPCloudClaimSchema, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { FPCloudClaim, FPCloudClaimSchema } from "@fireproof/core-types-protocols-cloud"; import { jwtVerify } from "jose"; import { JWKPublic } from "@fireproof/core-types-base"; +export interface TokenAndClaims { + readonly token: string; + readonly claims: FPCloudClaim; +} export async function convertToTokenAndClaims( dashApi: { getClerkPublishableKey(): Promise<{ cloudPublicKeys: JWKPublic[] }>; @@ -24,7 +28,7 @@ export async function convertToTokenAndClaims( } const rFPCloudClaim = FPCloudClaimSchema.safeParse(rUnknownClaims.Ok().payload); if (!rFPCloudClaim.success) { - logger.Warn().Err(rFPCloudClaim.error).Msg("Token claims validation failed"); + logger.Warn().Err(rFPCloudClaim.error).Any({ inObj: rUnknownClaims.Ok().payload }).Msg("Token claims validation failed"); continue; } return Result.Ok({ diff --git a/cloud/connector/base/tsconfig.json b/cloud/connector/base/tsconfig.json new file mode 100644 index 000000000..a7db7c68a --- /dev/null +++ b/cloud/connector/base/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/cloud/connector/iframe/clerk-fpcc-evt-entity.ts b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts index 6493cd17c..db2f5db47 100644 --- a/cloud/connector/iframe/clerk-fpcc-evt-entity.ts +++ b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts @@ -3,7 +3,7 @@ import { SuperThis } from "@fireproof/core-types-base"; import { DashApi, AuthType, isResCloudDbTokenBound } from "@fireproof/core-protocols-dashboard"; import { BackendFPCC, GetCloudDbTokenResult } from "./iframe-fpcc-protocol.js"; import { ensureLogger, exceptionWrapper, sleep } from "@fireproof/core-runtime"; -import { TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; import { Clerk } from "@clerk/clerk-js/headless"; import { DbKey, FPCCEvtApp, FPCCMsgBase, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; @@ -79,7 +79,7 @@ export class ClerkFPCCEvtEntity implements BackendFPCC { if (!clerk.user) { return Result.Err(new Error("User not logged in")); } - const token = await clerk.session?.getToken(); + const token = await clerk.session?.getToken({ template: "with-email" }); if (!token) { return Result.Err(new Error("No session token available")); } @@ -131,8 +131,8 @@ export class ClerkFPCCEvtEntity implements BackendFPCC { } // - async waitForAuthToken(resultId: string): Promise> { - return poller(async () => { + async waitForAuthToken(resultId: string): Promise> { + return poller(async () => { const clerk = await clerkSvc(this.dashApi); if (!clerk.user) { return { @@ -182,7 +182,11 @@ export class ClerkFPCCEvtEntity implements BackendFPCC { }); } - async getTokenForDb(dbInfo: DbKey, authToken: TokenAndClaims, originEvt: Partial): Promise { + async getTokenForDb( + dbInfo: DbKey, + authToken: TokenAndSelectedTenantAndLedger, + originEvt: Partial, + ): Promise { await sleep(50); return { ...dbInfo, diff --git a/cloud/connector/iframe/iframe-fpcc-protocol.ts b/cloud/connector/iframe/iframe-fpcc-protocol.ts index 391b74e85..f50066327 100644 --- a/cloud/connector/iframe/iframe-fpcc-protocol.ts +++ b/cloud/connector/iframe/iframe-fpcc-protocol.ts @@ -24,7 +24,7 @@ import { ResCloudDbTokenNotBound, isResCloudDbTokenBound, } from "@fireproof/core-protocols-dashboard"; -import { TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; import { ClerkFPCCEvtEntity } from "./clerk-fpcc-evt-entity.js"; export interface IframeFPCCProtocolOpts { @@ -36,7 +36,7 @@ export interface IframeFPCCProtocolOpts { export type GetCloudDbTokenResult = | { readonly res: ResCloudDbTokenBound; - readonly claims: TokenAndClaims["claims"]; + readonly claims: TokenAndSelectedTenantAndLedger["claims"]; } | { readonly res: ResCloudDbTokenNotBound; @@ -48,7 +48,7 @@ export interface BackendFPCC { isFPCCEvtAppReady(): boolean; getState(): "needs-login" | "waiting" | "ready"; setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready"; - waitForAuthToken(resultId: string): Promise>; + waitForAuthToken(resultId: string): Promise>; getFPCCEvtApp(): Promise>; setFPCCEvtApp(app: FPCCEvtApp): Promise; isUserLoggedIn(): Promise; diff --git a/cloud/connector/iframe/index.ts b/cloud/connector/iframe/index.ts index 40f74ed8e..8a8085089 100644 --- a/cloud/connector/iframe/index.ts +++ b/cloud/connector/iframe/index.ts @@ -1,3 +1,5 @@ export * from "./clerk-fpcc-evt-entity.js"; export * from "./fp-cloud-connector.js"; export * from "./iframe-fpcc-protocol.js"; + +export * from "./injected-iframe.js"; diff --git a/cloud/connector/iframe/injected-iframe.html b/cloud/connector/iframe/injected-iframe.html index 18b636c33..6fd9d047b 100644 --- a/cloud/connector/iframe/injected-iframe.html +++ b/cloud/connector/iframe/injected-iframe.html @@ -5,17 +5,8 @@ I'm the Fireproof Cloud Connector diff --git a/cloud/connector/iframe/injected-iframe.tsx b/cloud/connector/iframe/injected-iframe.tsx new file mode 100644 index 000000000..e0d0fe6fc --- /dev/null +++ b/cloud/connector/iframe/injected-iframe.tsx @@ -0,0 +1,47 @@ +import { renderToString } from "preact-render-to-string"; +import { createElement } from "preact"; +// import { BuildURI, loadAsset } from "@adviser/cement"; +import type { fpCloudConnector } from "./fp-cloud-connector.js"; + +export const React = { + createElement, +}; + +async function scriptFpCloudConnect() { + const script = () => { + let fpccJS; + // vite does strange things to import + // in this case the iframe is not running in a vite runtime + try { + // eslint-disable-next-line no-restricted-globals + const url = new URL("fp-cloud-connector.js", window.location.href); + + fpccJS = import(/* @vite-ignore */ url.toString()); + // eslint-disable-next-line no-console + console.log("loaded -- js", url.toString()); + } catch (e) { + // eslint-disable-next-line no-restricted-globals + const url = new URL("fp-cloud-connector.ts", window.location.href); + fpccJS = import(/* @vite-ignore */ url.toString()); + // eslint-disable-next-line no-console + console.log("loaded -- ts", url.toString(), fpccJS); + } + fpccJS + .then((fpcc: { fpCloudConnector: typeof fpCloudConnector }) => fpcc.fpCloudConnector(window.location.href)) + // eslint-disable-next-line no-console + .then(() => console.log("injected-iframe-ready", window.location.href)); + }; + return `(${script.toString().replace(/__vite_ssr_dynamic_import__/, "import")})()`; +} + +export async function injectedHtml() { + return renderToString( + + Fireproof Cloud Connector + + I'm the Fireproof Cloud Connector + + + , + ); +} diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json index 4ecf4e76c..576a7f66b 100644 --- a/cloud/connector/iframe/package.json +++ b/cloud/connector/iframe/package.json @@ -32,11 +32,13 @@ }, "dependencies": { "@adviser/cement": "^0.4.53", + "@clerk/clerk-js": "^5.102.0", + "@fireproof/cloud-connector-base": "workspace:*", + "@fireproof/core-protocols-dashboard": "workspace:*", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", - "@fireproof/core-protocols-dashboard": "workspace:*", "@fireproof/core-types-protocols-cloud": "workspace:*", - "@fireproof/cloud-connector-base": "workspace:*", - "@clerk/clerk-js": "^5.102.0" + "preact": "^10.27.2", + "preact-render-to-string": "^6.6.2" } } diff --git a/cloud/connector/iframe/tsconfig.json b/cloud/connector/iframe/tsconfig.json new file mode 100644 index 000000000..a7db7c68a --- /dev/null +++ b/cloud/connector/iframe/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/cloud/connector/page/tsconfig.json b/cloud/connector/page/tsconfig.json new file mode 100644 index 000000000..a7db7c68a --- /dev/null +++ b/cloud/connector/page/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/cloud/connector/test/package.json b/cloud/connector/test/package.json index d1103ce66..54eb46b59 100644 --- a/cloud/connector/test/package.json +++ b/cloud/connector/test/package.json @@ -6,8 +6,8 @@ "private": "true", "scripts": { "build": "core-cli tsc", - "pack": "core-cli build --doPack", - "publish": "core-cli build", + "pack": "echo skip", + "publish": "echo skip", "test": "vitest --run" }, "keywords": [ diff --git a/cloud/connector/test/tsconfig.json b/cloud/connector/test/tsconfig.json new file mode 100644 index 000000000..a7db7c68a --- /dev/null +++ b/cloud/connector/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/core/gateways/cloud/to-cloud.ts b/core/gateways/cloud/to-cloud.ts index 195f9259b..22e6d00b4 100644 --- a/core/gateways/cloud/to-cloud.ts +++ b/core/gateways/cloud/to-cloud.ts @@ -11,7 +11,7 @@ import { ToCloudOptionalOpts, ToCloudOpts, ToCloudRequiredOpts, - TokenAndClaims, + TokenAndSelectedTenantAndLedger, TokenStrategie, } from "@fireproof/core-types-protocols-cloud"; import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; @@ -31,7 +31,7 @@ function addTenantAndLedger(opts: ToCloudOptionalOpts, uri: CoerceURI): URI { } export class SimpleTokenStrategy implements TokenStrategie { - private tc: TokenAndClaims; + private tc: TokenAndSelectedTenantAndLedger; constructor(jwk: string) { let claims: FPCloudClaim; try { @@ -74,7 +74,7 @@ export class SimpleTokenStrategy implements TokenStrategie { // // console.log("SimpleTokenStrategy gatherToken"); // return this.tc; // } - async waitForToken(): Promise> { + async waitForToken(): Promise> { // console.log("SimpleTokenStrategy waitForToken"); return Result.Ok(this.tc); } diff --git a/core/types/protocols/cloud/gateway-control.ts b/core/types/protocols/cloud/gateway-control.ts index 8e72e1b32..161cbc239 100644 --- a/core/types/protocols/cloud/gateway-control.ts +++ b/core/types/protocols/cloud/gateway-control.ts @@ -1,15 +1,21 @@ import { Logger, CoerceURI, URI, AppContext, Result } from "@adviser/cement"; import { Attachable, SuperThis } from "@fireproof/core-types-base"; -import { FPCloudClaim } from "./msg-types.zod.js"; +// import { FPCloudClaim } from "./msg-types.zod.js"; export interface ToCloudAttachable extends Attachable { token?: string; readonly opts: ToCloudOpts; } -export interface TokenAndClaims { +export interface TokenAndSelectedTenantAndLedger { readonly token: string; - readonly claims: FPCloudClaim; + readonly claims: { + readonly selected: { + readonly tenant: string; + readonly ledger: string; + }; + }; + //FPCloudClaim; // readonly exp: number; // readonly tenant?: string; // readonly ledger?: string; @@ -20,7 +26,12 @@ export interface TokenStrategie { hash(): string; open(sthis: SuperThis, logger: Logger, localDbName: string, opts: ToCloudOpts): void; // tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; - waitForToken(sthis: SuperThis, logger: Logger, localDbName: string, opts: ToCloudOpts): Promise>; + waitForToken( + sthis: SuperThis, + logger: Logger, + localDbName: string, + opts: ToCloudOpts, + ): Promise>; stop(): void; } @@ -53,7 +64,7 @@ export function hashableFPCloudRef(ref?: Partial): { base?: string; export interface TokenAndClaimsEvents { hash(): string; - changed(token?: TokenAndClaims): Promise; + changed(token?: TokenAndSelectedTenantAndLedger): Promise; } export interface ToCloudRequiredOpts { diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index e8f651c49..fc67b3186 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -23,12 +23,32 @@ export default { async fetch(request: Request, env: Env) { const uri = URI.from(request.url); let ares: Promise; + console.log("cf-serve request", request.method, uri.toString()); switch (true) { case uri.pathname.startsWith("/api"): // console.log("cf-serve", request.url, env); ares = createHandler(drizzle(env.DB), env).then((fn) => fn(request) as unknown as Promise); break; + // case uri.pathname.startsWith("/fp-cloud-connector/injected-iframe.html"): { + // const html = await injectedHtml(); + // // console.log("cf-serve request", request.method, uri.toString(), "[",html, "]"); + // return new Response(html, { status: 200, headers: { "Content-Type": "text/html" } }); + // } + // break; + case uri.pathname === "/@fireproof/cloud-connector-iframe": { + return new Response("Redirecting...", { + status: 302, + headers: { + // in production it should point to esm.sh + Location: "/node_modules/@fireproof/cloud-connector-iframe/index.js", + }, + }); + } + // // return env.ASSETS.fetch(uri.build().pathname("@fireproof/cloud-connector-iframe").asURL(), request as unknown as CFRequest); + // } + // break; + case uri.pathname.startsWith("/.well-known/jwks.json"): ares = resWellKnownJwks(request, env as unknown as Record) as unknown as Promise; break; diff --git a/dashboard/backend/cloud-token.ts b/dashboard/backend/cloud-token.ts index c9dd632f6..b6b86b48a 100644 --- a/dashboard/backend/cloud-token.ts +++ b/dashboard/backend/cloud-token.ts @@ -88,6 +88,7 @@ async function createBoundToken( .where(and(eq(sqlLedgers.ledgerId, req.ledgerId), eq(sqlLedgers.tenantId, req.tenantId))) .get(); if (tandl) { + console.log("createBoundToken", auth); return Result.Ok({ type: "resCloudDbToken", status: "bound", diff --git a/dashboard/backend/db-api.test.ts b/dashboard/backend/db-api.test.ts index e064b71ae..50dedd357 100644 --- a/dashboard/backend/db-api.test.ts +++ b/dashboard/backend/db-api.test.ts @@ -1109,13 +1109,13 @@ describe("db-api", () => { const tandC = rTandC.Ok(); expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); expect(tandC.claims.selected.ledger).toBe(ledger.Ok().ledger.ledgerId); - expect(tandC.claims.ledgers).toEqual([ - { - id: ledger.Ok().ledger.ledgerId, - right: "write", - role: "admin", - }, - ]); + // expect(tandC.claims.ledgers).toEqual([ + // { + // id: ledger.Ok().ledger.ledgerId, + // right: "write", + // role: "admin", + // }, + // ]); }); it("with non existing ledger and tenant", async () => { const rRes = await fpApi.getCloudDbToken( diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 593041aa2..26ee44611 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -1,11 +1,43 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; // import { visualizer } from "rollup-plugin-visualizer"; +||||||| parent of 33350045 (chore: lets test the deployment) +import { defineConfig } from "vite"; +import { visualizer } from "rollup-plugin-visualizer"; import { dotenv } from "zx"; import { cloudflare } from "@cloudflare/vite-plugin"; import * as path from "path"; import * as fs from "fs"; +const serveFireproofAssets = (): Plugin => ({ + name: "serve-fireproof-assets", + + // Development server + configureServer(server) { + server.middlewares.use((req, res, next) => { + // Serve the HTML file + if (req.url?.startsWith("/@fireproof/cloud-connector-iframe/injected-iframe.html")) { + const htmlPath = path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/injected-iframe.html"); + const content = fs.readFileSync(htmlPath, "utf-8"); + res.setHeader("Content-Type", "text/html"); + res.end(content); + return; + } + next(); + }); + }, + + generateBundle() { + // Emit HTML file + const htmlPath = path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/injected-iframe.html"); + this.emitFile({ + type: "asset", + fileName: "fireproof/cloud-connector-iframe/injected-iframe.html", + source: fs.readFileSync(htmlPath, "utf-8"), + }); + }, +}); + function defines() { try { return Object.entries(dotenv.load(".dev.vars")).reduce( @@ -25,9 +57,12 @@ function defines() { // https://vitejs.dev/config/ export default defineConfig({ plugins: [ + serveFireproofAssets(), // multilines // tsconfigPaths(), - react(), + react({ + jsxRuntime: "classic", // Use classic instead of automatic + }), cloudflare(), // visualizer(), { @@ -78,12 +113,17 @@ export default defineConfig({ }, allowedHosts: ["localhost", "dev-local-1.adviser.com", "dev-local-2.adviser.com"], }, - resolve: process.env.USE_SOURCE - ? { - alias: { - "react-router": path.resolve(__dirname, "../../packages/react-router/index.js"), - "react-router-dom": path.resolve(__dirname, "../../packages/react-router-dom/index.jsx"), - }, - } - : {}, + resolve: { + // ...(process.env.USE_SOURCE + // ? { + // alias: { + // "react-router": path.resolve(__dirname, "../../packages/react-router/index.js"), + // "react-router-dom": path.resolve(__dirname, "../../packages/react-router-dom/index.jsx"), + // }, + // } + // : {}, + // ), + }, }); + +// console.log(">>>>>>", path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/index.ts")); diff --git a/patches/@clerk__clerk-js.patch b/patches/@clerk__clerk-js.patch new file mode 100644 index 000000000..4192aa190 --- /dev/null +++ b/patches/@clerk__clerk-js.patch @@ -0,0 +1,21 @@ +diff --git a/package.json b/package.json +index b710adceaf4b01d3c8b7eae2df52c4581c94f8e5..fe3567b54515024301e7ec819519b777f050eac2 100644 +--- a/package.json ++++ b/package.json +@@ -10,6 +10,16 @@ + "session", + "jwt" + ], ++ "exports": { ++ ".": { ++ "import": "./dist/clerk.mjs", ++ "types": "./dist/types/index.d.ts" ++ }, ++ "./headless": { ++ "import": "./headless/index.js", ++ "types": "./headless/index.d.ts" ++ } ++ }, + "homepage": "https://clerk.com/", + "bugs": { + "url": "https://github.com/clerk/javascript/issues" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7674f8419..24c75f33e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -467,6 +467,12 @@ importers: '@fireproof/core-types-protocols-cloud': specifier: workspace:* version: link:../../../core/types/protocols/cloud + preact: + specifier: ^10.27.2 + version: 10.27.2 + preact-render-to-string: + specifier: ^6.6.2 + version: 6.6.2(preact@10.27.2) cloud/connector/page: dependencies: diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index ba9aab3a4..6ba19fd2b 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,6 +1,6 @@ import { BuildURI, KeyedResolvOnce, Lazy, Logger, ResolveSeq, Result, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; -import { ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; +import { ToCloudOpts, TokenAndSelectedTenantAndLedger, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; import { ensureLogger, ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; @@ -54,11 +54,18 @@ export class FPCloudConnectStrategy implements TokenStrategie { constructor(opts: Partial = {}) { this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; - const fpCloudConnectURL = BuildURI.from( - opts.fpCloudConnectURL ?? + + const dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/"; + let fpCloudConnectURL: BuildURI; + if (opts.fpCloudConnectURL) { + fpCloudConnectURL = BuildURI.from(opts.fpCloudConnectURL); + } else { + fpCloudConnectURL = BuildURI.from( // eslint-disable-next-line no-restricted-globals - new URL("fp-cloud-connector/injected-iframe.html", import.meta.url).toString(), - ); + new URL("/", dashboardURI).toString(), + ).pathname("/@fireproof/cloud-connector-iframe/injected-iframe.html"); + } + if (opts.dashboardURI) { fpCloudConnectURL.setParam("dashboard_uri", opts.dashboardURI); } @@ -66,6 +73,7 @@ export class FPCloudConnectStrategy implements TokenStrategie { fpCloudConnectURL.setParam("cloud_api_uri", opts.cloudApiURI); } this.fpCloudConnectURL = fpCloudConnectURL.toString(); + // console.log("FPCloudConnectStrategy constructed with fpCloudConnectURL", this.fpCloudConnectURL); this.title = opts.title ?? "Fireproof Login"; this.sthis = opts.sthis ?? ensureSuperThis(); this.logger = ensureLogger(this.sthis, "FPCloudConnectStrategy"); @@ -136,10 +144,18 @@ export class FPCloudConnectStrategy implements TokenStrategie { readonly openloginSeq = new ResolveSeq(); // readonly waitForTokenPerLocalDbFuture = new KeyedResolvOnce>>(); - fpccEvtApp2TokenAndClaims(evt: FPCCEvtApp): Result { - const tAndC: TokenAndClaims = { + fpccEvtApp2TokenAndClaims(evt: FPCCEvtApp): Result { + // convertToTokenAndClaims({ + + // }, this.logger, evt.localDb.accessToken) + const tAndC: TokenAndSelectedTenantAndLedger = { token: evt.localDb.accessToken, - claims: {} as TokenAndClaims["claims"], + claims: { + selected: { + tenant: evt.localDb.tenantId, + ledger: evt.localDb.ledgerId, + }, + }, }; return Result.Ok(tAndC); } @@ -196,7 +212,7 @@ export class FPCloudConnectStrategy implements TokenStrategie { // this.waitState = "stopped"; } - readonly waitForTokenAndClaims = new KeyedResolvOnce>(); + readonly waitForTokenAndClaims = new KeyedResolvOnce>(); // async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { // console.log("FPCloudConnectStrategy tryToken called", opts); // // if (!this.currentToken) { @@ -208,7 +224,12 @@ export class FPCloudConnectStrategy implements TokenStrategie { // return undefined; // } - async waitForToken(_sthis: SuperThis, _logger: Logger, localDbName: string, _opts: ToCloudOpts): Promise> { + async waitForToken( + _sthis: SuperThis, + _logger: Logger, + localDbName: string, + _opts: ToCloudOpts, + ): Promise> { // console.log("FPCloudConnectStrategy waitForToken called for localDbName", localDbName); const ppage = await this.getPageProtocol(this.sthis); const key = dbAppKey({ appId: ppage.getAppId(), dbName: localDbName }); diff --git a/use-fireproof/iframe-strategy.ts b/use-fireproof/iframe-strategy.ts index 2c75dda8e..fe0289035 100644 --- a/use-fireproof/iframe-strategy.ts +++ b/use-fireproof/iframe-strategy.ts @@ -1,6 +1,6 @@ import { BuildURI, Logger, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; -import { TokenStrategie, ToCloudOpts, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { TokenStrategie, ToCloudOpts, TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; import { WebCtx } from "./react/use-attach.js"; import { WebToCloudCtx } from "./react/types.js"; @@ -82,7 +82,7 @@ export class IframeStrategy implements TokenStrategie { const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; document.body.appendChild(this.overlayDiv(deviceId, redirectCtx.dashboardURI)); } - async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { + async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; // const uri = URI.from(window.location.href); // const uriFpToken = uri.getParam(redirectCtx.tokenParam); @@ -95,7 +95,7 @@ export class IframeStrategy implements TokenStrategie { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string): Promise> { + async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string): Promise> { // throw new Error("waitForToken not implemented"); return Result.Err("not implemented"); } diff --git a/use-fireproof/index.ts b/use-fireproof/index.ts index 478bf5f5d..814cb1358 100644 --- a/use-fireproof/index.ts +++ b/use-fireproof/index.ts @@ -12,7 +12,7 @@ import { TokenStrategie, TokenAndClaimsEvents, ToCloudAttachable, - TokenAndClaims, + TokenAndSelectedTenantAndLedger, } from "@fireproof/core-types-protocols-cloud"; import { WebToCloudCtx } from "./react/types.js"; import { defaultWebToCloudOpts, WebCtx } from "./react/use-attach.js"; @@ -52,7 +52,7 @@ export function toCloud(opts: UseFpToCloudParam = {}): ToCloudAttachable { const webCtx = defaultWebToCloudOpts(myOpts); if (!opts.events) { // hacky but who has a better idea? - myOpts.events.changed = async (token?: TokenAndClaims) => { + myOpts.events.changed = async (token?: TokenAndSelectedTenantAndLedger) => { if (token) { await webCtx.setToken(token); } else { diff --git a/use-fireproof/react/types.ts b/use-fireproof/react/types.ts index e154f21eb..fe4a08abb 100644 --- a/use-fireproof/react/types.ts +++ b/use-fireproof/react/types.ts @@ -19,7 +19,7 @@ import type { KeyBagIf, IndexRowsWithDocs, } from "@fireproof/core-types-base"; -import { ToCloudAttachable, TokenAndClaims } from "@fireproof/core-types-protocols-cloud"; +import { ToCloudAttachable, TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; export type LiveQueryResult = IndexRowsWithDocs; @@ -110,7 +110,7 @@ export interface InitialTokenAndClaimsState { } export interface ReadyTokenAndClaimsState { readonly state: "ready"; - readonly tokenAndClaims: TokenAndClaims; + readonly tokenAndClaims: TokenAndSelectedTenantAndLedger; readonly reset: () => void; } @@ -130,10 +130,10 @@ export interface WebToCloudCtx { ready(db: Database): Promise; - onTokenChange(on: (token?: TokenAndClaims) => void): void; + onTokenChange(on: (token?: TokenAndSelectedTenantAndLedger) => void): void; resetToken(): Promise; - setToken(token: TokenAndClaims | string): Promise; - token(): Promise; + setToken(token: TokenAndSelectedTenantAndLedger | string): Promise; + token(): Promise; } export type UseFPConfig = ConfigOpts & { readonly attach?: ToCloudAttachable }; diff --git a/use-fireproof/react/use-attach.ts b/use-fireproof/react/use-attach.ts index 88d84e5ef..983e922a4 100644 --- a/use-fireproof/react/use-attach.ts +++ b/use-fireproof/react/use-attach.ts @@ -10,7 +10,7 @@ import { FPCloudClaim, ToCloudAttachable, ToCloudOptionalOpts, - TokenAndClaims, + TokenAndSelectedTenantAndLedger, TokenStrategie, } from "@fireproof/core-types-protocols-cloud"; import { getKeyBag } from "@fireproof/core-keybag"; @@ -23,7 +23,7 @@ export type ToCloudParam = Omit & Partial & { readonly strategy?: TokenStrategie; readonly context?: AppContext }; class WebCtxImpl implements WebToCloudCtx { - readonly onActions = new Set<(token?: TokenAndClaims) => void>(); + readonly onActions = new Set<(token?: TokenAndSelectedTenantAndLedger) => void>(); readonly dashboardURI!: string; readonly tokenApiURI!: string; // readonly uiURI: string; @@ -72,13 +72,13 @@ class WebCtxImpl implements WebToCloudCtx { this.keyBag = this.keyBag ?? (await getKeyBag(this.sthis)); } - async onAction(token?: TokenAndClaims) { + async onAction(token?: TokenAndSelectedTenantAndLedger) { for (const action of this.onActions.values()) { action(token); } } - onTokenChange(on: (token?: TokenAndClaims) => void) { + onTokenChange(on: (token?: TokenAndSelectedTenantAndLedger) => void) { if (this.opts.onTokenChange) { return this.opts.onTokenChange(on); } @@ -92,7 +92,7 @@ class WebCtxImpl implements WebToCloudCtx { }; } - readonly _tokenAndClaims = new ResolveOnce(); + readonly _tokenAndClaims = new ResolveOnce(); async token() { if (this.opts.token) { @@ -126,7 +126,7 @@ class WebCtxImpl implements WebToCloudCtx { this.onAction(); } - async setToken(token: TokenAndClaims) { + async setToken(token: TokenAndSelectedTenantAndLedger) { if (this.opts.setToken) { return this.opts.setToken(token); } diff --git a/use-fireproof/redirect-strategy.ts b/use-fireproof/redirect-strategy.ts index de813823a..30afb1add 100644 --- a/use-fireproof/redirect-strategy.ts +++ b/use-fireproof/redirect-strategy.ts @@ -2,7 +2,7 @@ import { BuildURI, Lazy, Logger, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { decodeJwt } from "jose"; import DOMPurify from "dompurify"; -import { FPCloudClaim, ToCloudOpts, TokenAndClaims, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; +import { FPCloudClaim, ToCloudOpts, TokenAndSelectedTenantAndLedger, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; import { DashApi } from "@fireproof/core-protocols-dashboard"; import { WebToCloudCtx } from "./react/types.js"; import { WebCtx } from "./react/use-attach.js"; @@ -97,7 +97,7 @@ export class RedirectStrategy implements TokenStrategie { // window.location.href = url.toString(); } - private currentToken?: TokenAndClaims; + private currentToken?: TokenAndSelectedTenantAndLedger; waiting?: ReturnType; @@ -109,7 +109,7 @@ export class RedirectStrategy implements TokenStrategie { this.waitState = "stopped"; } - async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { + async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { if (!this.currentToken) { const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; this.currentToken = await webCtx.token(); @@ -123,7 +123,7 @@ export class RedirectStrategy implements TokenStrategie { dashApi: DashApi, resultId: undefined | string, opts: ToCloudOpts, - resolve: (value: TokenAndClaims) => void, + resolve: (value: TokenAndSelectedTenantAndLedger) => void, attempts = 0, ) { if (!resultId) { @@ -152,14 +152,19 @@ export class RedirectStrategy implements TokenStrategie { this.waiting = setTimeout(() => this.getTokenAndClaimsByResultId(logger, dashApi, resultId, opts, resolve), opts.intervalSec); } - async waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise> { + async waitForToken( + sthis: SuperThis, + logger: Logger, + deviceId: string, + opts: ToCloudOpts, + ): Promise> { if (!this.resultId) { throw new Error("waitForToken not working on redirect strategy"); } const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; const dashApi = new DashApi(webCtx.tokenApiURI); this.waitState = "started"; - return new Promise>((resolve) => { + return new Promise>((resolve) => { this.getTokenAndClaimsByResultId(logger, dashApi, this.resultId, opts, (tokenAndClaims) => { this.currentToken = tokenAndClaims; resolve(Result.Ok(tokenAndClaims)); diff --git a/vitest.config.ts b/vitest.config.ts index 4d2933683..a2c3b9873 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ "cloud/backend/cf-d1/vitest.config.ts", "cloud/backend/node/vitest.config.ts", "cloud/backend/base/vitest.config.ts", + "cloud/connector/test/vitest.config.ts", "cli/vitest.config.ts", "dashboard/vitest.config.ts", ], From 886bec70793b25aa088284a68b5c25f318033c90 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 12:32:11 +0100 Subject: [PATCH 10/23] formatting --- cloud/3rd-party/src/overlayHtml.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/3rd-party/src/overlayHtml.tsx b/cloud/3rd-party/src/overlayHtml.tsx index 502757c8e..7195172ad 100644 --- a/cloud/3rd-party/src/overlayHtml.tsx +++ b/cloud/3rd-party/src/overlayHtml.tsx @@ -4,7 +4,7 @@ import { createElement } from "preact"; const React = { createElement, }; - + // function jsxDEV(...args: unknown[]) { // // eslint-disable-next-line @typescript-eslint/no-explicit-any // return (React as any).call(React, ...args) From b61d6766cddb7d80b72db9a3e702d72b60dd125d Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 13:02:51 +0100 Subject: [PATCH 11/23] chore: fix tests --- .../connector/iframe/iframe-fpcc-protocol.ts | 2 +- dashboard/backend/api.ts | 18 +-- dashboard/backend/cloud-token.ts | 14 +- dashboard/backend/db-api.test.ts | 142 +++++++++++++++--- 4 files changed, 140 insertions(+), 36 deletions(-) diff --git a/cloud/connector/iframe/iframe-fpcc-protocol.ts b/cloud/connector/iframe/iframe-fpcc-protocol.ts index f50066327..fa3df5aeb 100644 --- a/cloud/connector/iframe/iframe-fpcc-protocol.ts +++ b/cloud/connector/iframe/iframe-fpcc-protocol.ts @@ -82,7 +82,7 @@ export class IframeFPCCProtocol implements FPCCProtocol { this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); this.dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud"; this.dashApiURI = opts.cloudApiURI ?? "https://dev.connect.fireproof.direct/api"; - console.log("IframeFPCCProtocol constructed with", opts); + // console.log("IframeFPCCProtocol constructed with", opts); this.dashApi = new DashApi(this.dashApiURI); } diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index e2ca42e08..ba234e3ea 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -417,9 +417,9 @@ export class FPApiSQL implements FPApiInterface { }) .where(and(eq(sqlTenantUsers.userId, req.userId), ne(sqlTenantUsers.default, 0))) .run(); - console.log("Clearing default tenant for user:", req.userId, req.tenantId, x); + // console.log("Clearing default tenant for user:", req.userId, req.tenantId, x); } - console.log("Adding user to tenant:", req.userId, "->", req.tenantId, "as", req.defaultTenant); + // console.log("Adding user to tenant:", req.userId, "->", req.tenantId, "as", req.defaultTenant); const ret = ( await db .insert(sqlTenantUsers) @@ -434,12 +434,12 @@ export class FPApiSQL implements FPApiInterface { }) .returning() )[0]; - const out = await db - .select() - .from(sqlTenantUsers) - .where(and(eq(sqlTenantUsers.userId, req.userId))) - .all(); - console.log("Added user to tenant:", out); + // const out = await db + // .select() + // .from(sqlTenantUsers) + // .where(and(eq(sqlTenantUsers.userId, req.userId))) + // .all(); + // console.log("Added user to tenant:", out); return Result.Ok({ userName: toUndef(ret.name), tenantName: tenant.name, @@ -1456,7 +1456,7 @@ export class FPApiSQL implements FPApiInterface { role: "admin", defaultTenant: req.tenant.defaultTenant ?? false, }); - console.log(`Created tenant ${tenant.tenantId} for user ${auth.user.userId}: ${req.tenant.defaultTenant}`); + // console.log(`Created tenant ${tenant.tenantId} for user ${auth.user.userId}: ${req.tenant.defaultTenant}`); return Result.Ok({ type: "resCreateTenant", tenant, diff --git a/dashboard/backend/cloud-token.ts b/dashboard/backend/cloud-token.ts index b6b86b48a..dce63a31a 100644 --- a/dashboard/backend/cloud-token.ts +++ b/dashboard/backend/cloud-token.ts @@ -88,7 +88,7 @@ async function createBoundToken( .where(and(eq(sqlLedgers.ledgerId, req.ledgerId), eq(sqlLedgers.tenantId, req.tenantId))) .get(); if (tandl) { - console.log("createBoundToken", auth); + // console.log("createBoundToken", auth); return Result.Ok({ type: "resCloudDbToken", status: "bound", @@ -175,7 +175,7 @@ export async function getCloudDbToken( } let tenantToCreateLedger: string; - console.log("No binding found for localDbName:", req.localDbName, "appId:", req.appId, "tenantId:", req.tenantId); + // console.log("No binding found for localDbName:", req.localDbName, "appId:", req.appId, "tenantId:", req.tenantId); if (!req.tenantId) { const rListLedgers = await api.listLedgersByUser({ type: "reqListLedgersByUser", @@ -185,7 +185,7 @@ export async function getCloudDbToken( return Result.Err(rListLedgers.Err()); } const ledgersPerTenant = rListLedgers.Ok().ledgers.filter((l) => l.name === req.localDbName); - console.log("Ledgers with names for user:", auth.user.userId, ledgersPerTenant); + // console.log("Ledgers with names for user:", auth.user.userId, ledgersPerTenant); if (ledgersPerTenant.length === 1) { return createBoundToken( { @@ -225,7 +225,7 @@ export async function getCloudDbToken( return logger.Error().Any({ userId: auth.user.userId }).Msg("User has no default tenant").ResultError(); } - console.log("Using default tenant for user:", auth.user.userId, "->", defaultTenant.tenantId, rListTenants.Ok().tenants); + // console.log("Using default tenant for user:", auth.user.userId, "->", defaultTenant.tenantId, rListTenants.Ok().tenants); tenantToCreateLedger = defaultTenant.tenantId; // get all ledgers for user @@ -405,9 +405,9 @@ export async function getCloudSessionToken( // }); } else if (req.resultId) { api.sthis.logger.Warn().Any({ resultId: req.resultId }).Msg("resultId too short"); - console.log("getCloudSessionToken-failed", { - result: req.resultId, - }); + // console.log("getCloudSessionToken-failed", { + // result: req.resultId, + // }); } // console.log(">>>>-post:", ctx, privKey) return Result.Ok({ diff --git a/dashboard/backend/db-api.test.ts b/dashboard/backend/db-api.test.ts index 50dedd357..8885887dd 100644 --- a/dashboard/backend/db-api.test.ts +++ b/dashboard/backend/db-api.test.ts @@ -1225,7 +1225,83 @@ describe("db-api", () => { expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); expect(tandC.claims.selected.ledger).toBe(ledger.Ok().ledger.ledgerId); }); - it("without ledger and tenant but appId and localDbName and ambiguity", async () => { + // it("without ledger and tenant but appId and localDbName and ambiguity", async () => { + // const tenant = await fpApi.createTenant({ + // type: "reqCreateTenant", + // auth: data[0].reqs.auth, + // tenant: { + // defaultTenant: true, + // // ownerUserId: data[0].ress.user.userId, + // }, + // }); + // const name = `DB Token Ledger-${sthis.nextId(6).str}`; + // await fpApi.createLedger({ + // type: "reqCreateLedger", + // auth: data[0].reqs.auth, + // ledger: { + // tenantId: tenant.Ok().tenant.tenantId, + // name, + // }, + // }); + // await fpApi.createLedger({ + // type: "reqCreateLedger", + // auth: data[0].reqs.auth, + // ledger: { + // tenantId: tenant.Ok().tenant.tenantId, + // name, + // }, + // }); + + // const rRes = await fpApi.getCloudDbToken( + // { + // type: "reqCloudDbToken", + // auth: data[0].reqs.auth, + // tenantId: tenant.Ok().tenant.tenantId, + // localDbName: name, + // appId: "not-existing-app", + // deviceId: "not-existing-device", + // }, + // ctx, + // ); + // const res = rRes.Ok(); + // if (!isResCloudDbTokenBound(res)) { + // assert.fail("Expected not bound response"); + // return; + // } + // const rTandC = await convertToTokenAndClaims( + // { + // getClerkPublishableKey(): Promise { + // return Promise.resolve({ + // type: "resClerkPublishableKey", + // publishableKey: "undefined", + // cloudPublicKeys: [publicKey], + // }); + // }, + // }, + // logger, + // res.token, + // ); + // const tandC = rTandC.Ok(); + // // console.log(data.map((i) => i.ress.tenants).map((j) => j.map((k) => k.tenantId))); + // expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); + // expect(tandC.claims.selected.ledger).toBe("xxx"); //ledger.Ok().ledger.ledgerId); + // }); + it("without ledger but tenant appId and localDbName and no ambiguity", async () => { + const ledgerName = `DB Token Ledger-${sthis.nextId(6).str}`; + + const otherTenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: {}, + }); + await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: otherTenant.Ok().tenant.tenantId, + name: ledgerName, + }, + }); const tenant = await fpApi.createTenant({ type: "reqCreateTenant", auth: data[0].reqs.auth, @@ -1234,30 +1310,65 @@ describe("db-api", () => { // ownerUserId: data[0].ress.user.userId, }, }); - const name = `DB Token Ledger-${sthis.nextId(6).str}`; - await fpApi.createLedger({ + const ledger = await fpApi.createLedger({ type: "reqCreateLedger", auth: data[0].reqs.auth, ledger: { tenantId: tenant.Ok().tenant.tenantId, - name, + name: ledgerName, }, }); - await fpApi.createLedger({ - type: "reqCreateLedger", - auth: data[0].reqs.auth, - ledger: { + const rRes = await fpApi.getCloudDbToken( + { + type: "reqCloudDbToken", + auth: data[0].reqs.auth, tenantId: tenant.Ok().tenant.tenantId, - name, + localDbName: ledgerName, + appId: "not-existing-app", + deviceId: "not-existing-device", }, - }); + ctx, + ); + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + assert.fail("Expected not bound response"); + return; + } + const rTandC = await convertToTokenAndClaims( + { + getClerkPublishableKey(): Promise { + return Promise.resolve({ + type: "resClerkPublishableKey", + publishableKey: "undefined", + cloudPublicKeys: [publicKey], + }); + }, + }, + logger, + res.token, + ); + const tandC = rTandC.Ok(); + // console.log(data.map((i) => i.ress.tenants).map((j) => j.map((k) => k.tenantId))); + expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); + expect(tandC.claims.selected.ledger).toBe(ledger.Ok().ledger.ledgerId); //ledger.Ok().ledger.ledgerId); + }); + it("without ledger but tenant appId and localDbName implicit create", async () => { + const ledgerName = `DB Token Ledger-${sthis.nextId(6).str}`; + const tenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: { + defaultTenant: true, + // ownerUserId: data[0].ress.user.userId, + }, + }); const rRes = await fpApi.getCloudDbToken( { type: "reqCloudDbToken", auth: data[0].reqs.auth, tenantId: tenant.Ok().tenant.tenantId, - localDbName: name, + localDbName: ledgerName, appId: "not-existing-app", deviceId: "not-existing-device", }, @@ -1284,15 +1395,8 @@ describe("db-api", () => { const tandC = rTandC.Ok(); // console.log(data.map((i) => i.ress.tenants).map((j) => j.map((k) => k.tenantId))); expect(tandC.claims.selected.tenant).toBe(tenant.Ok().tenant.tenantId); - expect(tandC.claims.selected.ledger).toBe("xxx"); //ledger.Ok().ledger.ledgerId); - }); - it("without ledger but tenant appId and localDbName and no ambiguity", async () => { - /* empty */ - }); - it("without ledger but tenant appId and localDbName and ambiguity", async () => { - /* empty */ + expect(tandC.claims.selected.ledger).toBeDefined(); }); - // }); }); it("queryEmail strips +....@", async () => { From f3ade928bb332de0c562e3122002dfb5377ba327 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 13:14:19 +0100 Subject: [PATCH 12/23] chore: cleanup clerk/clerk-js --- dashboard/src/pages/cloud.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/dashboard/src/pages/cloud.tsx b/dashboard/src/pages/cloud.tsx index f36fc91db..8d6a119dd 100644 --- a/dashboard/src/pages/cloud.tsx +++ b/dashboard/src/pages/cloud.tsx @@ -1,6 +1,4 @@ import React, { useContext } from "react"; -// import { Clerk } from "@clerk/clerk-js"; -// import { Clerk } from "@clerk/clerk-react"; import { Navigate, NavLink, useLocation, useParams } from "react-router-dom"; import { AppContext } from "../app-context.jsx"; import { Plus } from "../components/Plus.jsx"; From f5975b06f7840ce70b6b2eb8da23d227f49628dc Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 13:21:17 +0100 Subject: [PATCH 13/23] chore: fix lint error --- dashboard/backend/api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index ba234e3ea..80d2c3d66 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -409,7 +409,7 @@ export class FPApiSQL implements FPApiInterface { } const now = new Date().toISOString(); if (req.defaultTenant) { - const x = await db + await db .update(sqlTenantUsers) .set({ default: 0, @@ -417,7 +417,6 @@ export class FPApiSQL implements FPApiInterface { }) .where(and(eq(sqlTenantUsers.userId, req.userId), ne(sqlTenantUsers.default, 0))) .run(); - // console.log("Clearing default tenant for user:", req.userId, req.tenantId, x); } // console.log("Adding user to tenant:", req.userId, "->", req.tenantId, "as", req.defaultTenant); const ret = ( From 9675ae3fc4368157cc7434bc28bae151d333e867 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 13:23:50 +0100 Subject: [PATCH 14/23] chore: dashboard prettier --- dashboard/package.json | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dashboard/package.json b/dashboard/package.json index 5efab1b65..10ed36832 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -12,6 +12,8 @@ "build:vite": "vite build", "preview": "vite preview", "test": "vitest --run", + "format": "prettier .", + "lint": "eslint", "check": "pnpm format --write && core-cli tsc --noEmit && pnpm lint && pnpm test && pnpm build", "drizzle:libsql": "drizzle-kit push --config ./drizzle.libsql.config.ts", "drizzle:d1-local": "drizzle-kit push --config ./drizzle.d1-local-backend.config.ts", diff --git a/package.json b/package.json index 0925f8fb8..956ced001 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "test:file": "vitest --config vitest.file.config.ts --run", "test:indexeddb": "vitest --config vitest.indexeddb.config.ts --run", "test:deno": "deno run --quiet --allow-net --allow-write --allow-run --allow-sys --allow-ffi --allow-read --allow-env ./node_modules/vitest/vitest.mjs --run --project core:file", - "format": "prettier .", "check": "pnpm format --write --log-level silent && pnpm lint --quiet && pnpm test && pnpm build", + "format": "prettier .", "lint": "eslint", "drizzle:libsql": "drizzle-kit push --config ./cloud/backend/node/drizzle.cloud.libsql.config.ts", "drizzle:d1-local": "drizzle-kit push --config ./cloud/backend/cf-d1/drizzle.cloud.d1-local.config.ts", From 7dae888be31617125edc5a6816e6d1e99080022b Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 14:38:31 +0100 Subject: [PATCH 15/23] chore: version update of vitest --- cli/build-cmd.ts | 4 +- cloud/connector/test/package.json | 6 +- cloud/connector/test/vitest.config.ts | 12 +- core/tests/vitest.indexeddb.config.ts | 6 - pnpm-lock.yaml | 227 ++++++++++++++++++-------- smoke/esm/package-template.json | 13 +- smoke/esm/src/index.test.ts | 2 +- smoke/esm/vitest.config.ts | 7 +- smoke/react/package-template.json | 12 +- smoke/react/vite.config.ts | 9 +- 10 files changed, 189 insertions(+), 109 deletions(-) diff --git a/cli/build-cmd.ts b/cli/build-cmd.ts index d13a9dfc6..5a6a9dd95 100644 --- a/cli/build-cmd.ts +++ b/cli/build-cmd.ts @@ -560,11 +560,13 @@ export function buildCmd(sthis: SuperThis) { // await $`env | grep -e npm_config -e NPM_CONFIG -e PNPM_CONFIG`; } const tags = args.pubTags; + console.log("version ---- ", args.version); try { const semVer = new SemVer(args.version); - if (semVer.prerelease.find((i) => typeof i === "string" && i.includes("dev"))) { + if (semVer.prerelease.find((i) => typeof i === "string")) { tags.push("dev"); } + console.log("version ---- ", args.version, semVer, tags); } catch (e) { console.warn(`Warn parsing version ${args.version}:`, e); } diff --git a/cloud/connector/test/package.json b/cloud/connector/test/package.json index 54eb46b59..ba6d38d4c 100644 --- a/cloud/connector/test/package.json +++ b/cloud/connector/test/package.json @@ -36,7 +36,9 @@ "@fireproof/cloud-connector-iframe": "workspace:*", "@fireproof/cloud-connector-page": "workspace:*", "@fireproof/core-runtime": "workspace:*", - "@vitest/browser": "^3.2.4", - "ts-essentials": "^10.1.1" + "@vitest/browser": "^4.0.4", + "@vitest/browser-playwright": "^4.0.4", + "ts-essentials": "^10.1.1", + "vitest": "^4.0.4" } } diff --git a/cloud/connector/test/vitest.config.ts b/cloud/connector/test/vitest.config.ts index 6558607e3..1d1b73653 100644 --- a/cloud/connector/test/vitest.config.ts +++ b/cloud/connector/test/vitest.config.ts @@ -1,7 +1,5 @@ -/// -/// - import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; export default defineConfig({ test: { @@ -11,14 +9,12 @@ export default defineConfig({ browser: { enabled: true, headless: true, - provider: "playwright", + provider: playwright({ + // ...custom playwright options + }), instances: [ { browser: "chromium", - context: { - recordVideo: undefined, - recordHar: undefined, - }, }, ], screenshotFailures: false, diff --git a/core/tests/vitest.indexeddb.config.ts b/core/tests/vitest.indexeddb.config.ts index b6c631fc7..c86bc3823 100644 --- a/core/tests/vitest.indexeddb.config.ts +++ b/core/tests/vitest.indexeddb.config.ts @@ -13,12 +13,6 @@ export default defineConfig({ instances: [ { browser: "chromium", - //setupFile: './chromium-setup.js', - // context: { - // // Disable screenshots and video recording - // recordVideo: undefined, - // recordHar: undefined, - // }, }, ], screenshotFailures: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24c75f33e..352ea2c74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -507,11 +507,17 @@ importers: specifier: workspace:* version: link:../../../core/runtime '@vitest/browser': - specifier: ^3.2.4 - version: 3.2.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8) + specifier: ^4.0.4 + version: 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) + '@vitest/browser-playwright': + specifier: ^4.0.4 + version: 4.0.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) ts-essentials: specifier: ^10.1.1 version: 10.1.1(typescript@5.9.3) + vitest: + specifier: ^4.0.4 + version: 4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) cloud/todo-app: dependencies: @@ -3256,12 +3262,6 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - '@tsd/typescript@5.9.2': resolution: {integrity: sha512-mSMM0QtEPdMd+rdMDd17yCUYD4yI3pKHap89+jEZrZ3KIO5PhDofBjER0OtgHdvOXF74KMLO3fyD6k3Hz0v03A==} engines: {node: '>=14.17'} @@ -3450,40 +3450,39 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/browser-playwright@4.0.4': + resolution: {integrity: sha512-jGKnGZ5ZKXuwQ1Ldwll/rZxk3webz4gz3kvoTYX2NH2ASPiwFGck8D09Sf2wVjCuDqebPXXd69zUIt1o4yQ5tA==} + peerDependencies: + playwright: '*' + vitest: 4.0.4 + '@vitest/browser-playwright@4.0.8': resolution: {integrity: sha512-MUi0msIAPXcA2YAuVMcssrSYP/yylxLt347xyTC6+ODl0c4XQFs0d2AN3Pc3iTa0pxIGmogflUV6eogXpPbJeA==} peerDependencies: playwright: '*' vitest: 4.0.8 - '@vitest/browser@3.2.4': - resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} + '@vitest/browser@4.0.4': + resolution: {integrity: sha512-1ZXztcBtRd3maKliHzWbQohsyRjam0ws6OPRWNWfGxFUOHTlNBtDnJAm8z1x7IzVkZ6JcOAumHJAbxNJh4tkDw==} peerDependencies: - playwright: '*' - safaridriver: '*' - vitest: 3.2.4 - webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 - peerDependenciesMeta: - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true + vitest: 4.0.4 '@vitest/browser@4.0.8': resolution: {integrity: sha512-oG6QJAR0d7S5SDnIYZwjxCj/a5fhbp9ZE7GtMgZn+yCUf4CxtqbBV6aXyg0qmn8nbUWT+rGuXL2ZB6qDBUjv/A==} peerDependencies: vitest: 4.0.8 + '@vitest/expect@4.0.4': + resolution: {integrity: sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==} + '@vitest/expect@4.0.8': resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==} - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + '@vitest/mocker@4.0.4': + resolution: {integrity: sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true @@ -3501,26 +3500,32 @@ packages: vite: optional: true - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.0.4': + resolution: {integrity: sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==} '@vitest/pretty-format@4.0.8': resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==} + '@vitest/runner@4.0.4': + resolution: {integrity: sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==} + '@vitest/runner@4.0.8': resolution: {integrity: sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==} + '@vitest/snapshot@4.0.4': + resolution: {integrity: sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==} + '@vitest/snapshot@4.0.8': resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==} - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.0.4': + resolution: {integrity: sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==} '@vitest/spy@4.0.8': resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==} - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.0.4': + resolution: {integrity: sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==} '@vitest/utils@4.0.8': resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} @@ -4939,9 +4944,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5809,18 +5811,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -6041,6 +6035,40 @@ packages: yaml: optional: true + vitest@4.0.4: + resolution: {integrity: sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.4 + '@vitest/browser-preview': 4.0.4 + '@vitest/browser-webdriverio': 4.0.4 + '@vitest/ui': 4.0.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.0.8: resolution: {integrity: sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7764,10 +7792,6 @@ snapshots: '@types/react': 19.2.3 '@types/react-dom': 19.2.3(@types/react@19.2.3) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': - dependencies: - '@testing-library/dom': 10.4.1 - '@tsd/typescript@5.9.2': {} '@types/aria-query@5.0.4': {} @@ -7996,6 +8020,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/browser-playwright@4.0.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4)': + dependencies: + '@vitest/browser': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) + '@vitest/mocker': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + playwright: 1.56.1 + tinyrainbow: 3.0.3 + vitest: 4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser-playwright@4.0.8(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8)': dependencies: '@vitest/browser': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8) @@ -8009,19 +8046,17 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8)': + '@vitest/browser@4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4)': dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) - '@vitest/utils': 3.2.4 + '@vitest/mocker': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/utils': 4.0.4 magic-string: 0.30.21 + pixelmatch: 7.1.0 + pngjs: 7.0.0 sirv: 3.0.2 - tinyrainbow: 2.0.0 - vitest: 4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + tinyrainbow: 3.0.3 + vitest: 4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) ws: 8.18.3 - optionalDependencies: - playwright: 1.56.1 transitivePeerDependencies: - bufferutil - msw @@ -8045,6 +8080,15 @@ snapshots: - utf-8-validate - vite + '@vitest/expect@4.0.4': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.4 + '@vitest/utils': 4.0.4 + chai: 6.2.0 + tinyrainbow: 3.0.3 + '@vitest/expect@4.0.8': dependencies: '@standard-schema/spec': 1.0.0 @@ -8054,9 +8098,9 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))': + '@vitest/mocker@4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))': dependencies: - '@vitest/spy': 3.2.4 + '@vitest/spy': 4.0.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -8070,36 +8114,44 @@ snapshots: optionalDependencies: vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - '@vitest/pretty-format@3.2.4': + '@vitest/pretty-format@4.0.4': dependencies: - tinyrainbow: 2.0.0 + tinyrainbow: 3.0.3 '@vitest/pretty-format@4.0.8': dependencies: tinyrainbow: 3.0.3 + '@vitest/runner@4.0.4': + dependencies: + '@vitest/utils': 4.0.4 + pathe: 2.0.3 + '@vitest/runner@4.0.8': dependencies: '@vitest/utils': 4.0.8 pathe: 2.0.3 + '@vitest/snapshot@4.0.4': + dependencies: + '@vitest/pretty-format': 4.0.4 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/snapshot@4.0.8': dependencies: '@vitest/pretty-format': 4.0.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 + '@vitest/spy@4.0.4': {} '@vitest/spy@4.0.8': {} - '@vitest/utils@3.2.4': + '@vitest/utils@4.0.4': dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.0.4 + tinyrainbow: 3.0.3 '@vitest/utils@4.0.8': dependencies: @@ -9696,8 +9748,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.2.1: {} - lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -10668,12 +10718,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyrainbow@2.0.0: {} - tinyrainbow@3.0.3: {} - tinyspy@4.0.4: {} - tmp@0.2.5: {} to-regex-range@5.0.1: @@ -10878,6 +10924,45 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 + vitest@4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.4 + '@vitest/mocker': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.4 + '@vitest/runner': 4.0.4 + '@vitest/snapshot': 4.0.4 + '@vitest/spy': 4.0.4 + '@vitest/utils': 4.0.4 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.1 + '@vitest/browser-playwright': 4.0.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.8 diff --git a/smoke/esm/package-template.json b/smoke/esm/package-template.json index df350526a..e6cabc569 100644 --- a/smoke/esm/package-template.json +++ b/smoke/esm/package-template.json @@ -9,12 +9,13 @@ "devDependencies": { "deno": "^2.5.0", "typescript": "^5.9.0", - "playwright": "^1.55.1", - "playwright-chromium": "^1.55.1", - "@vitest/browser": "^3.2.4", - "@vitest/ui": "^3.2.4", - "vitest": "^3.2.4", - "vite": "^7.1.0" + "playwright": "^1.56.1", + "playwright-chromium": "^1.56.1", + "@vitest/browser": "^4.0.4", + "@vitest/browser-playwright": "^4.0.4", + "@vitest/ui": "^4.0.4", + "vitest": "^4.0.4", + "vite": "^7.1.12" }, "pnpm": { "onlyBuiltDependencies": ["esbuild", "msw", "playwright-chromium", "deno"] diff --git a/smoke/esm/src/index.test.ts b/smoke/esm/src/index.test.ts index 6de2f9427..384bc23d8 100644 --- a/smoke/esm/src/index.test.ts +++ b/smoke/esm/src/index.test.ts @@ -1,4 +1,4 @@ -import { page } from "@vitest/browser/context"; +import { page } from "vitest/browser"; import { expect, it, vi } from "vitest"; interface FPWindow extends Window { diff --git a/smoke/esm/vitest.config.ts b/smoke/esm/vitest.config.ts index 142d38be5..986ada14b 100644 --- a/smoke/esm/vitest.config.ts +++ b/smoke/esm/vitest.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; export default defineConfig({ test: { @@ -6,12 +7,12 @@ export default defineConfig({ setupFiles: "./setup.js", browser: { enabled: true, - provider: "playwright", - // provider: "webdriverio", headless: true, + provider: playwright({ + // ...custom playwright options + }), instances: [ { - // browser: "chrome", browser: "chromium", }, ], diff --git a/smoke/react/package-template.json b/smoke/react/package-template.json index 65c3c497f..585b05c9b 100644 --- a/smoke/react/package-template.json +++ b/smoke/react/package-template.json @@ -22,13 +22,13 @@ "@testing-library/react": "^16.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", - "playwright": "^1.55.0", - "playwright-chromium": "^1.55.0", + "playwright": "^1.56.1", "@vitejs/plugin-react": "^5.0.4", - "@vitest/browser": "^3.2.4", - "@vitest/ui": "^3.2.4", + "@vitest/browser": "^4.0.4", + "@vitest/browser-playwright": "^4.0.4", + "@vitest/ui": "^4.0.4", "typescript": "^5.9.0", - "vite": "^7.1.0", - "vitest": "^3.2.4" + "vite": "^7.1.12", + "vitest": "^4.0.4" } } diff --git a/smoke/react/vite.config.ts b/smoke/react/vite.config.ts index 29cc3fa26..43ffb9d73 100644 --- a/smoke/react/vite.config.ts +++ b/smoke/react/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; export default defineConfig({ test: { @@ -7,16 +8,14 @@ export default defineConfig({ browser: { enabled: true, headless: true, - provider: "playwright", - // provider: "webdriverio", + provider: playwright({ + // ...custom playwright options + }), instances: [ { - // browser: "chrome", browser: "chromium", }, ], - // name: "chrome", // browser name is required - // Disable screenshots providerOptions: { use: { screenshot: "off", From e4aa9f53ef099bc022bf11e24d696a39ee9f4bf0 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 15:28:51 +0100 Subject: [PATCH 16/23] chore: fix build errors --- cloud/connector/iframe/index.ts | 2 - cloud/connector/iframe/injected-iframe.tsx | 47 ----------------- cloud/connector/iframe/package.json | 4 +- dashboard/.prettierignore | 7 +++ dashboard/backend/cf-serve.ts | 17 +++--- dashboard/package.json | 2 + dashboard/vite.config.ts | 60 +++++++++++++++++++--- pnpm-lock.yaml | 11 ++-- 8 files changed, 73 insertions(+), 77 deletions(-) delete mode 100644 cloud/connector/iframe/injected-iframe.tsx create mode 100644 dashboard/.prettierignore diff --git a/cloud/connector/iframe/index.ts b/cloud/connector/iframe/index.ts index 8a8085089..40f74ed8e 100644 --- a/cloud/connector/iframe/index.ts +++ b/cloud/connector/iframe/index.ts @@ -1,5 +1,3 @@ export * from "./clerk-fpcc-evt-entity.js"; export * from "./fp-cloud-connector.js"; export * from "./iframe-fpcc-protocol.js"; - -export * from "./injected-iframe.js"; diff --git a/cloud/connector/iframe/injected-iframe.tsx b/cloud/connector/iframe/injected-iframe.tsx deleted file mode 100644 index e0d0fe6fc..000000000 --- a/cloud/connector/iframe/injected-iframe.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { renderToString } from "preact-render-to-string"; -import { createElement } from "preact"; -// import { BuildURI, loadAsset } from "@adviser/cement"; -import type { fpCloudConnector } from "./fp-cloud-connector.js"; - -export const React = { - createElement, -}; - -async function scriptFpCloudConnect() { - const script = () => { - let fpccJS; - // vite does strange things to import - // in this case the iframe is not running in a vite runtime - try { - // eslint-disable-next-line no-restricted-globals - const url = new URL("fp-cloud-connector.js", window.location.href); - - fpccJS = import(/* @vite-ignore */ url.toString()); - // eslint-disable-next-line no-console - console.log("loaded -- js", url.toString()); - } catch (e) { - // eslint-disable-next-line no-restricted-globals - const url = new URL("fp-cloud-connector.ts", window.location.href); - fpccJS = import(/* @vite-ignore */ url.toString()); - // eslint-disable-next-line no-console - console.log("loaded -- ts", url.toString(), fpccJS); - } - fpccJS - .then((fpcc: { fpCloudConnector: typeof fpCloudConnector }) => fpcc.fpCloudConnector(window.location.href)) - // eslint-disable-next-line no-console - .then(() => console.log("injected-iframe-ready", window.location.href)); - }; - return `(${script.toString().replace(/__vite_ssr_dynamic_import__/, "import")})()`; -} - -export async function injectedHtml() { - return renderToString( - - Fireproof Cloud Connector - - I'm the Fireproof Cloud Connector - - - , - ); -} diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json index 576a7f66b..0a442ce3e 100644 --- a/cloud/connector/iframe/package.json +++ b/cloud/connector/iframe/package.json @@ -37,8 +37,6 @@ "@fireproof/core-protocols-dashboard": "workspace:*", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", - "@fireproof/core-types-protocols-cloud": "workspace:*", - "preact": "^10.27.2", - "preact-render-to-string": "^6.6.2" + "@fireproof/core-types-protocols-cloud": "workspace:*" } } diff --git a/dashboard/.prettierignore b/dashboard/.prettierignore new file mode 100644 index 000000000..0360df56a --- /dev/null +++ b/dashboard/.prettierignore @@ -0,0 +1,7 @@ +**/pnpm-lock.yaml +scripts/ +**/.cache/** +**/.esm-cache/** +**/dist/** +**/coverage/** +dist/ diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index fc67b3186..913f8f8d5 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -23,7 +23,6 @@ export default { async fetch(request: Request, env: Env) { const uri = URI.from(request.url); let ares: Promise; - console.log("cf-serve request", request.method, uri.toString()); switch (true) { case uri.pathname.startsWith("/api"): // console.log("cf-serve", request.url, env); @@ -36,16 +35,14 @@ export default { // return new Response(html, { status: 200, headers: { "Content-Type": "text/html" } }); // } // break; - case uri.pathname === "/@fireproof/cloud-connector-iframe": { - return new Response("Redirecting...", { - status: 302, - headers: { - // in production it should point to esm.sh - Location: "/node_modules/@fireproof/cloud-connector-iframe/index.js", - }, - }); + case uri.pathname.startsWith("/@fireproof/cloud-connector-iframe"): { + if (uri.pathname === "/@fireproof/cloud-connector-iframe") { + console.log("module request", request.method, uri.toString()); + return env.ASSETS.fetch(uri.build().appendRelative("index.js").asURL(), request as unknown as CFRequest); + } + console.log("direct request", request.method, uri.toString()); + return env.ASSETS.fetch(request as unknown as CFRequest); } - // // return env.ASSETS.fetch(uri.build().pathname("@fireproof/cloud-connector-iframe").asURL(), request as unknown as CFRequest); // } // break; diff --git a/dashboard/package.json b/dashboard/package.json index 10ed36832..e88a923ca 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -60,6 +60,8 @@ "@cloudflare/workers-types": "^4.20251111.0", "@eslint/js": "^9.39.1", "@fireproof/core-cli": "workspace:0.0.0", + "@fireproof/cloud-connector-iframe": "workspace:0.0.0", + "@fireproof/core-cli": "workspace:*", "@libsql/client": "^0.15.15", "@libsql/kysely-libsql": "^0.4.1", "@rollup/plugin-replace": "^6.0.3", diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 26ee44611..0e7881cbe 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -8,33 +8,77 @@ import { dotenv } from "zx"; import { cloudflare } from "@cloudflare/vite-plugin"; import * as path from "path"; import * as fs from "fs"; +import * as esbuild from "esbuild"; const serveFireproofAssets = (): Plugin => ({ name: "serve-fireproof-assets", // Development server configureServer(server) { - server.middlewares.use((req, res, next) => { + server.middlewares.use(async (req, res, next) => { // Serve the HTML file - if (req.url?.startsWith("/@fireproof/cloud-connector-iframe/injected-iframe.html")) { - const htmlPath = path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/injected-iframe.html"); - const content = fs.readFileSync(htmlPath, "utf-8"); - res.setHeader("Content-Type", "text/html"); - res.end(content); + let url = req.url?.split("?")[0] || ""; + if (url.startsWith("/@fireproof")) { + if (url === "/@fireproof/cloud-connector-iframe") { + url += "/index.js"; + } + + const filePath = path.resolve(__dirname, `node_modules/${url}`); + if (url.endsWith(".html")) { + const content = await fs.promises.readFile(filePath, "utf-8"); + res.setHeader("Content-Type", "text/html"); + res.end(content); + return; + } + try { + // Use Vite's built-in transform + const result = await server.transformRequest(filePath); + if (result) { + res.setHeader("Content-Type", "application/javascript"); + res.end(result.code); + return; + } + } catch (e) { + res.statusCode = 500; + res.end(`Error: ${e}`); + return; + } + // console.log("serve-fireproof-assets", req.url, url); + + // const content = fs.readFileSync(htmlPath, "utf-8"); + // res.setHeader("Content-Type", "text/html"); + // res.end(content); return; } next(); }); }, - generateBundle() { + async generateBundle() { // Emit HTML file const htmlPath = path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/injected-iframe.html"); this.emitFile({ type: "asset", - fileName: "fireproof/cloud-connector-iframe/injected-iframe.html", + fileName: "@fireproof/cloud-connector-iframe/injected-iframe.html", source: fs.readFileSync(htmlPath, "utf-8"), }); + + const result = await esbuild.build({ + entryPoints: [path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/index.ts")], + bundle: true, + format: "esm", + write: false, + minify: false, + platform: "browser", + }); + + const bundledCode = result.outputFiles[0].text; + + this.emitFile({ + type: "asset", + fileName: "@fireproof/cloud-connector-iframe/index.js", + source: bundledCode, + }); }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 352ea2c74..a95dde2f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -467,12 +467,6 @@ importers: '@fireproof/core-types-protocols-cloud': specifier: workspace:* version: link:../../../core/types/protocols/cloud - preact: - specifier: ^10.27.2 - version: 10.27.2 - preact-render-to-string: - specifier: ^6.6.2 - version: 6.6.2(preact@10.27.2) cloud/connector/page: dependencies: @@ -1287,8 +1281,11 @@ importers: '@eslint/js': specifier: ^9.39.1 version: 9.39.1 - '@fireproof/core-cli': + '@fireproof/cloud-connector-iframe': specifier: workspace:0.0.0 + version: link:../cloud/connector/iframe + '@fireproof/core-cli': + specifier: workspace:* version: link:../cli '@libsql/client': specifier: ^0.15.15 From ea3c228d50e027a09ae56a6aba7d9ec290721c12 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 16:01:32 +0100 Subject: [PATCH 17/23] chore: try to find the problem why the iframe is not loggin --- cloud/3rd-party/src/App.tsx | 4 ++-- cloud/connector/iframe/clerk-fpcc-evt-entity.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/3rd-party/src/App.tsx b/cloud/3rd-party/src/App.tsx index a2bf9c420..dc490edcc 100644 --- a/cloud/3rd-party/src/App.tsx +++ b/cloud/3rd-party/src/App.tsx @@ -10,8 +10,8 @@ function App() { strategy: new FPCloudConnectStrategy({ // overlayCss: defaultOverlayCss, overlayHtml, - dashboardURI: "http://localhost:7370/fp/cloud/api/token", - cloudApiURI: "http://localhost:7370/api", + // dashboardURI: "http://localhost:7370/fp/cloud/api/token", + // cloudApiURI: "http://localhost:7370/api", }), // urls: { base: "fpcloud://localhost:8787?protocol=ws" }, // tenant: "3rd-party", diff --git a/cloud/connector/iframe/clerk-fpcc-evt-entity.ts b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts index db2f5db47..0162e3deb 100644 --- a/cloud/connector/iframe/clerk-fpcc-evt-entity.ts +++ b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts @@ -7,16 +7,16 @@ import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols import { Clerk } from "@clerk/clerk-js/headless"; import { DbKey, FPCCEvtApp, FPCCMsgBase, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; -const clerkSvc = Lazy(async (dashApi: DashApi) => { +export const clerkSvc = Lazy(async (dashApi: DashApi) => { const clerkPubKey = await dashApi.getClerkPublishableKey({}); // console.log("clerkSvc got publishable key", rClerkPubKey); const clerk = new Clerk(clerkPubKey.publishableKey); await clerk.load(); clerk.addListener((session) => { if (session.user) { - console.log("Iframe-Clerk-User signed in:", session.user); + console.log("Iframe-Clerk-User signed in:", session.user, window.location.href, clerkPubKey); } else { - console.log("Iframe-Clerk-User signed out"); + console.log("Iframe-Clerk-User signed out", window.location.href, clerkPubKey); } }); From 20b892a13de88930715f4dbd7703f5e9a7e7fcba Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 16:20:41 +0100 Subject: [PATCH 18/23] chore: try to get clerk running --- cloud/connector/iframe/iframe-fpcc-protocol.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/connector/iframe/iframe-fpcc-protocol.ts b/cloud/connector/iframe/iframe-fpcc-protocol.ts index fa3df5aeb..f37e196fb 100644 --- a/cloud/connector/iframe/iframe-fpcc-protocol.ts +++ b/cloud/connector/iframe/iframe-fpcc-protocol.ts @@ -25,7 +25,7 @@ import { isResCloudDbTokenBound, } from "@fireproof/core-protocols-dashboard"; import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; -import { ClerkFPCCEvtEntity } from "./clerk-fpcc-evt-entity.js"; +import { ClerkFPCCEvtEntity, clerkSvc } from "./clerk-fpcc-evt-entity.js"; export interface IframeFPCCProtocolOpts { readonly dashboardURI: string; @@ -389,6 +389,7 @@ export class IframeFPCCProtocol implements FPCCProtocol { } async ready(): Promise { + await clerkSvc(this.dashApi); await this.fpccProtocol.ready(); this.fpccProtocol.onFPCCMessage(this.handleFPCCMessage); return this; From f8e32f71797eab558011c64fe5b49e46b6bb55ab Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 28 Oct 2025 16:53:02 +0100 Subject: [PATCH 19/23] chore: check localStorage-sharing --- cloud/connector/iframe/iframe-fpcc-protocol.ts | 3 +++ dashboard/src/components/App.tsx | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/cloud/connector/iframe/iframe-fpcc-protocol.ts b/cloud/connector/iframe/iframe-fpcc-protocol.ts index f37e196fb..d3c6d6d22 100644 --- a/cloud/connector/iframe/iframe-fpcc-protocol.ts +++ b/cloud/connector/iframe/iframe-fpcc-protocol.ts @@ -390,6 +390,9 @@ export class IframeFPCCProtocol implements FPCCProtocol { async ready(): Promise { await clerkSvc(this.dashApi); + setInterval(() => { + localStorage.setItem("iframe", new Date().toISOString()); + }, 10000) await this.fpccProtocol.ready(); this.fpccProtocol.onFPCCMessage(this.handleFPCCMessage); return this; diff --git a/dashboard/src/components/App.tsx b/dashboard/src/components/App.tsx index e9e114665..827df3ea8 100644 --- a/dashboard/src/components/App.tsx +++ b/dashboard/src/components/App.tsx @@ -41,6 +41,10 @@ import { Index, indexLoader } from "../pages/index.jsx"; import { Login, loginLoader } from "../pages/login.jsx"; import { SignUpPage, signupLoader } from "../pages/signup.jsx"; +setInterval(() => { + localStorage.setItem("app", `${new Date().toISOString()}:${window.location.href}`); +}, 10000); + export function App() { const ctx = useContext(AppContext); // console.log(">>>>>>>>>>>>>>", window.location.href); From 8f7f5170209fc08509dcb3b5725b70db89fb1b5c Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 29 Oct 2025 09:46:44 +0100 Subject: [PATCH 20/23] update @adviser/cement --- cloud/connector/base/package.json | 2 +- cloud/connector/iframe/package.json | 2 +- cloud/connector/page/package.json | 4 ++-- pnpm-lock.yaml | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cloud/connector/base/package.json b/cloud/connector/base/package.json index e8f6b6be6..7f631c320 100644 --- a/cloud/connector/base/package.json +++ b/cloud/connector/base/package.json @@ -30,7 +30,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.53", + "@adviser/cement": "^0.4.54", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", "@fireproof/core-types-protocols-cloud": "workspace:*", diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json index 0a442ce3e..fae0accd4 100644 --- a/cloud/connector/iframe/package.json +++ b/cloud/connector/iframe/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.53", + "@adviser/cement": "^0.4.54", "@clerk/clerk-js": "^5.102.0", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-protocols-dashboard": "workspace:*", diff --git a/cloud/connector/page/package.json b/cloud/connector/page/package.json index a92c76d75..ab8026ef2 100644 --- a/cloud/connector/page/package.json +++ b/cloud/connector/page/package.json @@ -31,10 +31,10 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.53", + "@adviser/cement": "^0.4.54", "@fireproof/cloud-connector-base": "workspace:*", - "@fireproof/core-types-base": "workspace:*", "@fireproof/core-runtime": "workspace:*", + "@fireproof/core-types-base": "workspace:*", "ts-essentials": "^10.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a95dde2f1..8d6a51ec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -423,7 +423,7 @@ importers: cloud/connector/base: dependencies: '@adviser/cement': - specifier: ^0.4.53 + specifier: ^0.4.54 version: 0.4.63(typescript@5.9.3) '@fireproof/core-runtime': specifier: workspace:* @@ -447,7 +447,7 @@ importers: cloud/connector/iframe: dependencies: '@adviser/cement': - specifier: ^0.4.53 + specifier: ^0.4.54 version: 0.4.63(typescript@5.9.3) '@clerk/clerk-js': specifier: ^5.102.0 @@ -471,7 +471,7 @@ importers: cloud/connector/page: dependencies: '@adviser/cement': - specifier: ^0.4.53 + specifier: ^0.4.54 version: 0.4.63(typescript@5.9.3) '@fireproof/cloud-connector-base': specifier: workspace:* From 467e6d845a75d3da854f309f068962c4571f2831 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 4 Nov 2025 16:05:34 +0100 Subject: [PATCH 21/23] wip [skip ci] --- cloud/3rd-party/src/App.tsx | 8 +- cloud/3rd-party/src/main.tsx | 1 + cloud/3rd-party/src/overlayHtml.tsx | 10 +- cloud/connector/base/fpcc-protocol.ts | 132 +++- cloud/connector/base/index.ts | 13 + cloud/connector/base/package.json | 2 +- .../connector/iframe/clerk-fpcc-evt-entity.ts | 216 ------ cloud/connector/iframe/fp-cloud-connector.ts | 33 - .../connector/iframe/iframe-fpcc-protocol.ts | 406 ------------ cloud/connector/iframe/index.ts | 3 - cloud/connector/iframe/injected-iframe.html | 2 +- cloud/connector/iframe/package.json | 2 +- cloud/connector/page/package.json | 2 +- cloud/connector/page/page-fpcc-protocol.ts | 65 +- cloud/connector/page/page-handler.ts | 10 +- cloud/connector/test/package.json | 1 + .../connector/test/page-fpcc-protocol.test.ts | 4 +- core/gateways/cloud/to-cloud.ts | 1 + core/protocols/dashboard/msg-api.ts | 6 +- core/types/protocols/cloud/gateway-control.ts | 2 + dashboard/backend/cf-serve.ts | 8 +- dashboard/vite.config.ts | 8 +- pnpm-lock.yaml | 627 ++++++++++++++++++ .../fp-cloud-connect-strategy-impl.ts | 166 +++++ use-fireproof/fp-cloud-connect-strategy.ts | 428 ++++++------ .../iframe-fp-cloud-connect-strategy.ts | 74 +++ use-fireproof/iframe-strategy.ts | 1 + use-fireproof/index.ts | 2 + use-fireproof/redirect-strategy.ts | 2 +- use-fireproof/window-open-fp-cloud.ts | 106 +++ 30 files changed, 1374 insertions(+), 967 deletions(-) delete mode 100644 cloud/connector/iframe/clerk-fpcc-evt-entity.ts delete mode 100644 cloud/connector/iframe/fp-cloud-connector.ts delete mode 100644 cloud/connector/iframe/iframe-fpcc-protocol.ts delete mode 100644 cloud/connector/iframe/index.ts create mode 100644 use-fireproof/fp-cloud-connect-strategy-impl.ts create mode 100644 use-fireproof/iframe-fp-cloud-connect-strategy.ts create mode 100644 use-fireproof/window-open-fp-cloud.ts diff --git a/cloud/3rd-party/src/App.tsx b/cloud/3rd-party/src/App.tsx index dc490edcc..5ae6ec471 100644 --- a/cloud/3rd-party/src/App.tsx +++ b/cloud/3rd-party/src/App.tsx @@ -1,17 +1,17 @@ import { DocWithId, useFireproof, toCloud, FPCloudConnectStrategy } from "use-fireproof"; import React, { useState, useEffect } from "react"; import "./App.css"; -import { overlayHtml } from "./overlayHtml.js"; +// import { overlayHtml } from "./overlayHtml.js"; // import { URI } from "@adviser/cement"; function App() { + const { database, attach } = useFireproof("fireproof-5-party", { attach: toCloud({ - strategy: new FPCloudConnectStrategy({ + strategy: FPCloudConnectStrategy({ // overlayCss: defaultOverlayCss, - overlayHtml, + // overlayHtml, // dashboardURI: "http://localhost:7370/fp/cloud/api/token", - // cloudApiURI: "http://localhost:7370/api", }), // urls: { base: "fpcloud://localhost:8787?protocol=ws" }, // tenant: "3rd-party", diff --git a/cloud/3rd-party/src/main.tsx b/cloud/3rd-party/src/main.tsx index 854ee1408..a42202c08 100644 --- a/cloud/3rd-party/src/main.tsx +++ b/cloud/3rd-party/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.jsx"; +console.log("i'm in main.tsx") // eslint-disable-next-line @typescript-eslint/no-non-null-assertion createRoot(document.getElementById("root")!).render( diff --git a/cloud/3rd-party/src/overlayHtml.tsx b/cloud/3rd-party/src/overlayHtml.tsx index 7195172ad..2d7a725ab 100644 --- a/cloud/3rd-party/src/overlayHtml.tsx +++ b/cloud/3rd-party/src/overlayHtml.tsx @@ -1,9 +1,9 @@ -import { renderToString } from "preact-render-to-string"; -import { createElement } from "preact"; +import { jsx } from "use-fireproof"; -const React = { - createElement, -}; +const { + renderToString, + React +} = jsx // function jsxDEV(...args: unknown[]) { // // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/cloud/connector/base/fpcc-protocol.ts b/cloud/connector/base/fpcc-protocol.ts index d3024aa2c..6aec9177b 100644 --- a/cloud/connector/base/fpcc-protocol.ts +++ b/cloud/connector/base/fpcc-protocol.ts @@ -1,15 +1,36 @@ -import { FPCCMessage, FPCCMsgBase, FPCCPong, FPCCSendMessage, isFPCCPing, validateFPCCMessage } from "./protocol-fp-cloud-conn.js"; -import { Logger } from "@adviser/cement"; +import { + FPCCError, + FPCCEvtApp, + FPCCEvtConnectorReady, + FPCCEvtNeedsLogin, + FPCCMessage, + FPCCMsgBase, + FPCCPing, + FPCCPong, + FPCCReqRegisterLocalDbName, + FPCCReqWaitConnectorReady, + FPCCSendMessage, + isFPCCError, + isFPCCEvtApp, + isFPCCEvtConnectorReady, + isFPCCEvtNeedsLogin, + isFPCCPing, + isFPCCPong, + isFPCCReqRegisterLocalDbName, + isFPCCReqWaitConnectorReady, + validateFPCCMessage, +} from "./protocol-fp-cloud-conn.js"; +import { Logger, OnFunc } from "@adviser/cement"; import { ensureLogger } from "@fireproof/core-runtime"; import { SuperThis } from "@fireproof/core-types-base"; export interface FPCCProtocol { // handle must be this bound method - handleMessage: (event: MessageEvent) => void; - handleFPCCMessage?: (event: FPCCMessage, srcEvent: MessageEvent) => void; + hash: () => string; + sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void; handleError: (error: unknown) => void; - injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void; + injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void; ready(): Promise; stop(): void; } @@ -17,13 +38,44 @@ export interface FPCCProtocol { export class FPCCProtocolBase implements FPCCProtocol { protected readonly sthis: SuperThis; protected readonly logger: Logger; - readonly #fpccMessageHandlers: ((msg: FPCCMessage, srcEvent: MessageEvent) => boolean | undefined)[] = []; readonly onStartFns: (() => void)[] = []; - #sendFn: ((msg: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage) | undefined = undefined; + #sendFn: ((msg: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage) | undefined = undefined; + + readonly onMessage = OnFunc<(event: MessageEvent) => void>(); + readonly onFPCCMessage = OnFunc<(msg: FPCCMessage, srcEvent: MessageEvent) => void>(); + + readonly onFPCCEvtNeedsLogin = OnFunc<(msg: FPCCEvtNeedsLogin, srcEvent: MessageEvent) => void>() + readonly onFPCCError = OnFunc<(msg: FPCCError, srcEvent: MessageEvent) => void>(); + readonly onFPCCReqRegisterLocalDbName = OnFunc<(msg: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent) => void>() + readonly onFPCCEvtApp = OnFunc<(msg: FPCCEvtApp, srcEvent: MessageEvent) => void>() + readonly onFPCCPing = OnFunc<(msg: FPCCPing, srcEvent: MessageEvent) => void>(); + readonly onFPCCPong = OnFunc<(msg: FPCCPong, srcEvent: MessageEvent) => void>() + readonly onFPCCEvtConnectorReady = OnFunc<(msg: FPCCEvtConnectorReady, srcEvent: MessageEvent) => void>(); + readonly onFPCCReqWaitConnectorReady = OnFunc<(msg: FPCCReqWaitConnectorReady, srcEvent: MessageEvent) => void>(); constructor(sthis: SuperThis, logger?: Logger) { this.sthis = sthis; this.logger = logger || ensureLogger(sthis, "FPCCProtocolBase"); + this.onMessage(event => { + this.handleMessage(event); + }); + this.onFPCCMessage((msg, srcEvent) => { + this.#handleFPCCMessage(msg, srcEvent); + }) + this.onFPCCPing((msg, srcEvent) => { + this.sendMessage({ + src: msg.dst, + dst: msg.src, + pingTid: msg.tid, + type: "FPCCPong", + }, + srcEvent, + ); + }); + } + + hash(): string { + throw new Error("should be implemented by subclass"); } handleMessage = (event: MessageEvent) => { @@ -34,55 +86,79 @@ export class FPCCProtocolBase implements FPCCProtocol { const fpCCmsg = validateFPCCMessage(event.data); // console.log("IframeFPCCProtocol handleMessage called", event.data, fpCCmsg.success); if (fpCCmsg.success) { - this.handleFPCCMessage(fpCCmsg.data, event); + this.onFPCCMessage.invoke(fpCCmsg.data, event); } else { this.logger.Warn().Err(fpCCmsg.error).Any("event", event).Msg("Received non-FPCC message"); } }; - onFPCCMessage(callback: (msg: FPCCMessage, srcEvent: MessageEvent) => boolean | undefined): void { - this.#fpccMessageHandlers.push(callback); - } - - handleFPCCMessage = (event: FPCCMessage, srcEvent: MessageEvent) => { - // allow handlers to process the message first and abort further processing - if (this.#fpccMessageHandlers.map((handler) => handler(event, srcEvent)).some((handled) => handled)) { - return; - } + #handleFPCCMessage(event: FPCCMessage, srcEvent: MessageEvent) { this.logger.Debug().Any("event", event).Msg("Handling FPCC message"); switch (true) { + + case isFPCCEvtNeedsLogin(event): { + this.onFPCCEvtNeedsLogin.invoke(event, srcEvent); + break; + } + + case isFPCCError(event): { + this.onFPCCError.invoke(event, srcEvent); + break; + } + + case isFPCCReqRegisterLocalDbName(event): { + this.onFPCCReqRegisterLocalDbName.invoke(event, srcEvent); + break; + } + + case isFPCCEvtApp(event): { + this.onFPCCEvtApp.invoke(event, srcEvent); + break; + } + case isFPCCPing(event): { - const pong: FPCCSendMessage = { - type: "FPCCPong", - dst: event.src, - pingTid: event.tid, - timestamp: Date.now(), - }; - this.sendMessage(pong, srcEvent); + this.onFPCCPing.invoke(event, srcEvent); + break; + } + + case isFPCCPong(event): { + this.onFPCCPong.invoke(event, srcEvent); + break; + } + + case isFPCCEvtConnectorReady(event): { + this.onFPCCEvtConnectorReady.invoke(event, srcEvent); + break; + } + + case isFPCCReqWaitConnectorReady(event): { + this.onFPCCReqWaitConnectorReady.invoke(event, srcEvent); break; } + } - }; + } handleError = (_error: unknown) => { throw new Error("Method not implemented."); }; ready(): Promise { + return Promise.resolve(this); } - injectSend(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + injectSend(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void { this.#sendFn = sendFn; } stop(): void { this.#sendFn = undefined; - this.#fpccMessageHandlers.splice(0, this.#fpccMessageHandlers.length); + this.onFPCCMessage.clear(); this.onStartFns.splice(0, this.onStartFns.length); } - sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent): T { + sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent | string): T { if (!this.#sendFn) { throw new Error("Protocol not started. Call start() before sending messages."); } diff --git a/cloud/connector/base/index.ts b/cloud/connector/base/index.ts index 8e7acc53c..0f5aa00ea 100644 --- a/cloud/connector/base/index.ts +++ b/cloud/connector/base/index.ts @@ -11,3 +11,16 @@ export interface DbKey { export function dbAppKey(o: DbKey): string { return o.appId + ":" + o.dbName; } + +export function isInIframe(win: { + readonly self: Window | null; + readonly top: Window | null; +} = window): boolean { + try { + return win.self !== win.top; + } catch (e) { + // If we can't access window.top due to cross-origin restrictions, + // we're definitely in an iframe + return true; + } +} diff --git a/cloud/connector/base/package.json b/cloud/connector/base/package.json index 7f631c320..79b29e172 100644 --- a/cloud/connector/base/package.json +++ b/cloud/connector/base/package.json @@ -30,7 +30,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.54", + "@adviser/cement": "^0.4.58", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", "@fireproof/core-types-protocols-cloud": "workspace:*", diff --git a/cloud/connector/iframe/clerk-fpcc-evt-entity.ts b/cloud/connector/iframe/clerk-fpcc-evt-entity.ts deleted file mode 100644 index 0162e3deb..000000000 --- a/cloud/connector/iframe/clerk-fpcc-evt-entity.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Lazy, Logger, poller, Result } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core-types-base"; -import { DashApi, AuthType, isResCloudDbTokenBound } from "@fireproof/core-protocols-dashboard"; -import { BackendFPCC, GetCloudDbTokenResult } from "./iframe-fpcc-protocol.js"; -import { ensureLogger, exceptionWrapper, sleep } from "@fireproof/core-runtime"; -import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; -import { Clerk } from "@clerk/clerk-js/headless"; -import { DbKey, FPCCEvtApp, FPCCMsgBase, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; - -export const clerkSvc = Lazy(async (dashApi: DashApi) => { - const clerkPubKey = await dashApi.getClerkPublishableKey({}); - // console.log("clerkSvc got publishable key", rClerkPubKey); - const clerk = new Clerk(clerkPubKey.publishableKey); - await clerk.load(); - clerk.addListener((session) => { - if (session.user) { - console.log("Iframe-Clerk-User signed in:", session.user, window.location.href, clerkPubKey); - } else { - console.log("Iframe-Clerk-User signed out", window.location.href, clerkPubKey); - } - }); - - return clerk; -}); - -// const clerkFPCCEvtEntities = new KeyedResolvOnce(); - -export class ClerkFPCCEvtEntity implements BackendFPCC { - readonly appId: string; - readonly dbName: string; - readonly deviceId: string; - readonly sthis: SuperThis; - readonly logger: Logger; - readonly dashApi: DashApi; - state: "needs-login" | "waiting" | "ready" = "needs-login"; - constructor(sthis: SuperThis, dashApi: DashApi, dbKey: DbKey, deviceId: string) { - this.logger = ensureLogger(sthis, `MemoryFPCCEvtEntity`, { - appId: dbKey.appId, - dbName: dbKey.dbName, - deviceId: deviceId, - }); - this.appId = dbKey.appId; - this.dbName = dbKey.dbName; - this.deviceId = deviceId; - this.sthis = sthis; - this.dashApi = dashApi; - } - - async getCloudDbToken(auth: AuthType): Promise> { - const rRes = await this.dashApi.getCloudDbToken({ - auth, - appId: this.appId, - localDbName: this.dbName, - deviceId: this.deviceId, - }); - if (rRes.isErr()) { - return Result.Err(rRes); - } - const res = rRes.Ok(); - if (!isResCloudDbTokenBound(res)) { - return Result.Ok({ res }); - } - const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); - if (rTandC.isErr()) { - return Result.Err(rTandC); - } - return Result.Ok({ res, claims: rTandC.Ok().claims }); - } - - isUserLoggedIn(): Promise { - return clerkSvc(this.dashApi).then((clerk) => { - return !!clerk.user; - }); - } - - async getDashApiToken(): Promise> { - return exceptionWrapper(async () => { - const clerk = await clerkSvc(this.dashApi); - if (!clerk.user) { - return Result.Err(new Error("User not logged in")); - } - const token = await clerk.session?.getToken({ template: "with-email" }); - if (!token) { - return Result.Err(new Error("No session token available")); - } - return Result.Ok({ - type: "clerk", - token, - }); - }); - } - - isFPCCEvtAppReady(): boolean { - // need to implement a check which looks into the token if it is expired or not - return this.state === "ready"; - } - - fpccEvtApp?: FPCCEvtApp; - getFPCCEvtApp(): Promise> { - return Promise.resolve(this.fpccEvtApp ? Result.Ok(this.fpccEvtApp) : Result.Err(new Error("No FPCCEvtApp registered"))); - } - - setFPCCEvtApp(app: FPCCEvtApp): Promise { - this.fpccEvtApp = app; - return Promise.resolve(); - } - getState(): "needs-login" | "waiting" | "ready" { - // For testing purposes, we always return "needs-login" - return this.state; - } - - // listRegisteredDbNames(): Promise { - // return Promise.all( - // clerkFPCCEvtEntities - // .values() - // .map((key) => { - // return key.value; - // }) - // .filter((v) => v.isOk()) - // .map((v) => v.Ok().getFPCCEvtApp()), - // ).then((apps) => { - // console.log("listRegisteredDbNames-o", apps); - // return apps.filter((res) => res.isOk()).map((res) => res.Ok()); - // }); - // } - - setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready" { - const prev = this.state; - this.state = state; - return prev; - } - - // - async waitForAuthToken(resultId: string): Promise> { - return poller(async () => { - const clerk = await clerkSvc(this.dashApi); - if (!clerk.user) { - return { - state: "waiting", - }; - } - // console.log("clerk user is logged in:", clerk.user); - - const rWaitForToken = await this.dashApi.waitForToken({ resultId }, this.logger); - if (rWaitForToken.isErr()) { - return { - state: "error", - error: rWaitForToken.Err(), - }; - } - const waitedTokenByResultId = rWaitForToken.unwrap(); - if (waitedTokenByResultId.status === "found" && waitedTokenByResultId.token) { - const token = waitedTokenByResultId.token; - if (!token) { - return { - state: "error", - error: new Error("No token received"), - }; - } - const rTokenClaims = await convertToTokenAndClaims(this.dashApi, this.logger, token); - if (rTokenClaims.isErr()) { - return { - state: "error", - error: rTokenClaims.Err(), - }; - } - return { - state: "success", - result: rTokenClaims.Ok(), - }; - } - return { state: "waiting" }; - }).then((res) => { - switch (res.state) { - case "success": - return Result.Ok(res.result); - case "error": - return Result.Err(res.error); - default: - return Result.Err("should not happen"); - } - }); - } - - async getTokenForDb( - dbInfo: DbKey, - authToken: TokenAndSelectedTenantAndLedger, - originEvt: Partial, - ): Promise { - await sleep(50); - return { - ...dbInfo, - tid: originEvt.tid ?? this.sthis.nextId(12).str, - type: "FPCCEvtApp", - src: "fp-cloud-connector", - dst: originEvt.src ?? "iframe", - appFavIcon: { - defURL: "https://example.com/favicon.ico", - }, - devId: this.deviceId, - user: { - name: "Test User", - email: "test@example.com", - provider: "google", - iconURL: "https://example.com/icon.png", - }, - localDb: { - dbName: dbInfo.dbName, - tenantId: "tenant-for-" + dbInfo.appId, - ledgerId: "ledger-for-" + dbInfo.appId, - accessToken: `auth-token-for-${dbInfo.appId}-${dbInfo.dbName}-with-${authToken}`, - }, - env: {}, - }; - } -} diff --git a/cloud/connector/iframe/fp-cloud-connector.ts b/cloud/connector/iframe/fp-cloud-connector.ts deleted file mode 100644 index a08153cf9..000000000 --- a/cloud/connector/iframe/fp-cloud-connector.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ensureSuperThis } from "@fireproof/core-runtime"; -import { BuildURI, Lazy, URI } from "@adviser/cement"; -import { FPCCMessage } from "@fireproof/cloud-connector-base"; -import { IframeFPCCProtocol } from "./iframe-fpcc-protocol.js"; - -export const fpCloudConnector = Lazy(async (loadUrlStr: string) => { - (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { - FP_DEBUG: "*", - }; - const sthis = ensureSuperThis(); - const loadUrl = URI.from(loadUrlStr); - const dashboardURI = loadUrl.getParam("dashboard_uri"); - let cloudApiURI = loadUrl.getParam("cloud_api_uri"); - if (dashboardURI && !cloudApiURI) { - cloudApiURI = BuildURI.from(dashboardURI).pathname("/api").toString(); - } - - console.log("fpCloudConnector called with", loadUrlStr, { dashboardURI, cloudApiURI }); - - const protocol = new IframeFPCCProtocol(sthis, { - dashboardURI: dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud", - cloudApiURI: cloudApiURI ?? "https://dev.connect.fireproof.direct/api", - }); - window.addEventListener("message", protocol.handleMessage); - protocol.injectSend((event: FPCCMessage, srcEvent: MessageEvent) => { - (event as { src: string }).src = event.src ?? window.location.href; - // console.log("postMessager sending message", event); - srcEvent.source?.postMessage(event, { targetOrigin: srcEvent.origin }); - return event; - }); - await protocol.ready(); - return protocol; -}); diff --git a/cloud/connector/iframe/iframe-fpcc-protocol.ts b/cloud/connector/iframe/iframe-fpcc-protocol.ts deleted file mode 100644 index d3c6d6d22..000000000 --- a/cloud/connector/iframe/iframe-fpcc-protocol.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { ensureLogger, sleep } from "@fireproof/core-runtime"; -import { - convertToTokenAndClaims, - FPCCEvtApp, - FPCCEvtConnectorReady, - FPCCEvtNeedsLogin, - FPCCMessage, - FPCCMsgBase, - FPCCReqRegisterLocalDbName, - FPCCSendMessage, - isFPCCReqRegisterLocalDbName, - isFPCCReqWaitConnectorReady, - FPCCProtocol, - FPCCProtocolBase, - dbAppKey, - DbKey, -} from "@fireproof/cloud-connector-base"; -import { SuperThis } from "@fireproof/core-types-base"; -import { BuildURI, KeyedResolvSeq, Logger, Result } from "@adviser/cement"; -import { - DashApi, - AuthType, - ResCloudDbTokenBound, - ResCloudDbTokenNotBound, - isResCloudDbTokenBound, -} from "@fireproof/core-protocols-dashboard"; -import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; -import { ClerkFPCCEvtEntity, clerkSvc } from "./clerk-fpcc-evt-entity.js"; - -export interface IframeFPCCProtocolOpts { - readonly dashboardURI: string; - readonly cloudApiURI: string; - // readonly backend: BackendFPCC; -} - -export type GetCloudDbTokenResult = - | { - readonly res: ResCloudDbTokenBound; - readonly claims: TokenAndSelectedTenantAndLedger["claims"]; - } - | { - readonly res: ResCloudDbTokenNotBound; - }; -export interface BackendFPCC { - readonly appId: string; - readonly dbName: string; - readonly deviceId: string; - isFPCCEvtAppReady(): boolean; - getState(): "needs-login" | "waiting" | "ready"; - setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready"; - waitForAuthToken(resultId: string): Promise>; - getFPCCEvtApp(): Promise>; - setFPCCEvtApp(app: FPCCEvtApp): Promise; - isUserLoggedIn(): Promise; - getDashApiToken(): Promise>; - // listRegisteredDbNames(): Promise; - getCloudDbToken(auth: AuthType): Promise>; -} - -// function getBackendFromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): BackendFPCC { -// return ClerkFPCCEvtEntity.fromRegisterLocalDbName(sthis, dashApi, req, deviceId); -// } - -const registeredDbs = new Map(); - -// static fromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): ClerkFPCCEvtEntity { -// const key = dbAppKey(req); -// return clerkFPCCEvtEntities.get(key).once(() => new ClerkFPCCEvtEntity(sthis, dashApi, req, deviceId)); -// } - -export class IframeFPCCProtocol implements FPCCProtocol { - readonly sthis: SuperThis; - readonly logger: Logger; - readonly fpccProtocol: FPCCProtocolBase; - readonly dashboardURI: string; - readonly dashApiURI: string; - readonly dashApi: DashApi; - - constructor(sthis: SuperThis, opts: IframeFPCCProtocolOpts) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "IframeFPCCProtocol"); - this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); - this.dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud"; - this.dashApiURI = opts.cloudApiURI ?? "https://dev.connect.fireproof.direct/api"; - // console.log("IframeFPCCProtocol constructed with", opts); - this.dashApi = new DashApi(this.dashApiURI); - } - - registeredDb(key: DbKey) { - const mapKey = dbAppKey(key); - const existing = registeredDbs.get(mapKey); - if (existing) { - return existing; - } - const newEntity = new ClerkFPCCEvtEntity(this.sthis, this.dashApi, key, this.getDeviceId()); - registeredDbs.set(mapKey, newEntity); - return newEntity; - } - - readonly handleMessage = (event: MessageEvent): void => { - this.fpccProtocol.handleMessage(event); - }; - - getDeviceId(): string { - return "we-need-to-implement-device-id"; - } - - async requestPageToDoLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { - const loginTID = this.sthis.nextId(16).str; - const url = BuildURI.from(this.dashboardURI) - .setParam("back_url", "wait-for-token") // dummy back_url since we don't return to the app here - .setParam("result_id", loginTID) - .setParam("app_id", event.appId) - .setParam("local_ledger_name", event.dbName); - if (event.ledger) { - url.setParam("ledger", event.ledger); - } - if (event.tenant) { - url.setParam("tenant", event.tenant); - } - const fpccEvtNeedsLogin: FPCCSendMessage = { - tid: event.tid, - type: "FPCCEvtNeedsLogin", - dst: event.src, - devId: this.getDeviceId(), - loginURL: url.toString(), - loginTID, - loadDbNames: [event], - reason: "BindCloud", - }; - - this.sendMessage(fpccEvtNeedsLogin, srcEvent); - backend.waitForAuthToken(loginTID).then((rAuthToken) => { - if (rAuthToken.isErr()) { - this.logger.Error().Err(rAuthToken).Msg("Failed to obtain auth token after login"); - return; - } - return backend - .getCloudDbToken({ - type: "clerk", - token: rAuthToken.Ok().token, - }) - .then((rCloudToken) => { - if (rCloudToken.isErr()) { - throw this.logger - .Error() - .Err(rCloudToken) - .Any({ - appId: backend.appId, - dbName: backend.dbName, - }) - .Msg("Failed to obtain DB token after login") - .AsError(); - } - const cloudToken = rCloudToken.Ok(); - switch (cloudToken.res.status) { - case "not-bound": - throw this.logger - .Error() - .Str("status", cloudToken.res.status) - .Any({ - appId: backend.appId, - dbName: backend.dbName, - }) - .Msg("DB is still not bound after login") - .AsError(); - } - return cloudToken.res.token; - }) - .then((cloudToken) => convertToTokenAndClaims(this.dashApi, this.logger, cloudToken)) - .then((rTanc) => { - if (rTanc.isErr()) { - throw this.logger - .Error() - .Err(rTanc) - .Any({ - appId: backend.appId, - dbName: backend.dbName, - }) - .Msg("Failed to convert DB token to token and claims after login"); - } - const { claims, token } = rTanc.Ok(); - const fpccEvtApp = { - tid: event.tid, - dst: event.src, - type: "FPCCEvtApp", - appId: backend.appId, - appFavIcon: { - defURL: "https://fireproof.direct/favicon.ico", - }, - devId: "", - user: { - name: claims.nickname ?? claims.userId, - email: claims.email, - provider: claims.provider ?? "unknown", - iconURL: "https://fireproof.direct/favicon.ico", - }, - localDb: { - dbName: backend.dbName, - tenantId: claims.selected.tenant, - ledgerId: claims.selected.ledger, - accessToken: token, - }, - env: {}, // future env vars - } satisfies FPCCSendMessage; - backend.setState("ready"); - backend.setFPCCEvtApp(this.sendMessage(fpccEvtApp, srcEvent)); - // this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); - }); - }); - } - - readonly stateSeq = new KeyedResolvSeq(); - runStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { - return this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); - } - - listRegisteredDbs(): BackendFPCC[] { - return Array.from(registeredDbs.values()); - } - - async atomicRunStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { - const bstate = backend.getState(); - switch (true) { - case bstate === "ready" && isFPCCReqRegisterLocalDbName(event): - console.log("Backend is ready, sending FPCCEvtApp"); - return backend - .getFPCCEvtApp() - .then((rFpccEvtApp) => { - if (rFpccEvtApp.isOk()) { - this.sendMessage(rFpccEvtApp.Ok(), srcEvent); - } else { - this.logger.Error().Err(rFpccEvtApp).Msg("Failed to get FPCCEvtApp in ready state"); - } - }) - .then(() => Promise.resolve()); - case bstate === "waiting": - { - console.log("Backend is waiting"); - // this.logger.Info().Str("appID", event.appID).Msg("Backend is waiting"); - throw new Error("Backend is in waiting state; not implemented yet."); - } - break; - case bstate === "needs-login" && isFPCCReqRegisterLocalDbName(event): - { - console.log("Backend needs login", backend.appId, backend.dbName); - const rAuthToken = await backend.getDashApiToken(); - if (rAuthToken.isErr()) { - console.log("User not logged in, requesting login", backend.appId, backend.dbName); - // make all dbs go to waiting state - backend.setState("waiting"); - return this.requestPageToDoLogin(backend, event, srcEvent); - } else { - // const backend = this.registeredDb(event); - - if (backend.isFPCCEvtAppReady()) { - const rFpccEvtApp = await backend.getFPCCEvtApp(); - console.log("Backend is ready, sending FPCCEvtApp", backend.appId, backend.dbName, rFpccEvtApp); - if (rFpccEvtApp.isOk()) { - this.sendMessage(rFpccEvtApp.Ok(), srcEvent); - return; - } - } else { - const rDbToken = await this.dashApi.getCloudDbToken({ - auth: rAuthToken.Ok(), - appId: backend.appId, - localDbName: backend.dbName, - deviceId: backend.deviceId, - }); - if (rDbToken.isErr()) { - console.log("Failed to obtain DB token, requesting login", backend.appId, backend.dbName, rDbToken); - // make all dbs go to waiting state - backend.setState("waiting"); - await sleep(60000); - this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); - return; - } - if (rDbToken.Ok().status === "not-bound") { - console.log("DB is not bound, requesting login", backend.appId, backend.dbName); - // make all dbs go to waiting state - backend.setState("waiting"); - return this.requestPageToDoLogin(backend, event, srcEvent); - } else { - const rCloudToken = await backend.getCloudDbToken(rAuthToken.Ok()); - if (rCloudToken.isErr()) { - this.logger.Warn().Err(rCloudToken).Msg("Failed to obtain DB token, re-running state machine after delay"); - await sleep(1000); - this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); - return; - } - const res = rCloudToken.Ok().res; - if (!isResCloudDbTokenBound(res)) { - return this.requestPageToDoLogin(backend, event, srcEvent); - } - const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); - if (rTandC.isErr()) { - this.logger - .Warn() - .Err(rTandC) - .Msg("Failed to convert DB token to token and claims, re-running state machine after delay"); - await sleep(1000); - this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); - return; - } - const { token, claims } = rTandC.Ok(); - const fpccEvtApp: FPCCEvtApp = { - tid: event.tid, - type: "FPCCEvtApp", - src: "iframe", - dst: event.src, - appId: backend.appId, - appFavIcon: { - defURL: "https://fireproof.direct/favicon.ico", - }, - devId: backend.deviceId, - user: { - name: claims.nickname ?? claims.userId, - email: claims.email, - provider: claims.provider ?? "unknown", - iconURL: "https://fireproof.direct/favicon.ico", - }, - localDb: { - dbName: backend.dbName, - tenantId: claims.selected.tenant, - ledgerId: claims.selected.ledger, - accessToken: token, - }, - env: {}, - }; - await backend.setFPCCEvtApp(fpccEvtApp); - backend.setState("ready"); - console.log("Sent FPCCEvtApp after obtaining DB token", backend.appId, backend.dbName); - this.sendMessage(fpccEvtApp, srcEvent); - return; - } - } - } - } - break; - - default: - throw this.logger.Error().Str("state", bstate).Msg("Unknown backend state").AsError(); - } - } - - readonly handleFPCCMessage = (event: FPCCMessage, srcEvent: MessageEvent): boolean | undefined => { - try { - switch (true) { - case isFPCCReqRegisterLocalDbName(event): { - this.logger.Info().Any(event).Msg("Iframe-Received request to register app"); - const backend = this.registeredDb(event); - backend.setState("needs-login"); - console.log("Running state machine for register local db name", backend.getState()); - this.runStateMachine(backend, event, srcEvent); - break; - } - - case isFPCCReqWaitConnectorReady(event): { - this.logger.Info().Str("appID", event.appId).Msg("Received request to wait for connector ready"); - // Here you would implement logic to handle the wait for connector ready request - const readyEvent: FPCCSendMessage = { - type: "FPCCEvtConnectorReady", - timestamp: Date.now(), - seq: event.seq, - devId: this.getDeviceId(), - dst: event.src, - }; - this.sendMessage(readyEvent, srcEvent); - break; - } - } - } catch (error) { - this.logger.Error().Err(error).Msg("Error handling FPCC message"); - } - return undefined; - }; - - readonly handleError = (_error: unknown): void => { - throw new Error("Method not implemented."); - }; - - stop(): void { - console.log("IframeFPCCProtocol stop called"); - this.fpccProtocol.stop(); - } - - injectSend(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { - this.fpccProtocol.injectSend(sendFn); - } - - async ready(): Promise { - await clerkSvc(this.dashApi); - setInterval(() => { - localStorage.setItem("iframe", new Date().toISOString()); - }, 10000) - await this.fpccProtocol.ready(); - this.fpccProtocol.onFPCCMessage(this.handleFPCCMessage); - return this; - } - - sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent): T { - // message.src = window.location.href; - // console.log("IframeFPCCProtocol sendMessage called", message); - return this.fpccProtocol.sendMessage(message, srcEvent); - } -} diff --git a/cloud/connector/iframe/index.ts b/cloud/connector/iframe/index.ts deleted file mode 100644 index 40f74ed8e..000000000 --- a/cloud/connector/iframe/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./clerk-fpcc-evt-entity.js"; -export * from "./fp-cloud-connector.js"; -export * from "./iframe-fpcc-protocol.js"; diff --git a/cloud/connector/iframe/injected-iframe.html b/cloud/connector/iframe/injected-iframe.html index 6fd9d047b..0b0688262 100644 --- a/cloud/connector/iframe/injected-iframe.html +++ b/cloud/connector/iframe/injected-iframe.html @@ -5,7 +5,7 @@ I'm the Fireproof Cloud Connector diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json index fae0accd4..9126f49fd 100644 --- a/cloud/connector/iframe/package.json +++ b/cloud/connector/iframe/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.54", + "@adviser/cement": "^0.4.58", "@clerk/clerk-js": "^5.102.0", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-protocols-dashboard": "workspace:*", diff --git a/cloud/connector/page/package.json b/cloud/connector/page/package.json index ab8026ef2..5691f1ddd 100644 --- a/cloud/connector/page/package.json +++ b/cloud/connector/page/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.54", + "@adviser/cement": "^0.4.58", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", diff --git a/cloud/connector/page/page-fpcc-protocol.ts b/cloud/connector/page/page-fpcc-protocol.ts index 03389dd4a..952d3514d 100644 --- a/cloud/connector/page/page-fpcc-protocol.ts +++ b/cloud/connector/page/page-fpcc-protocol.ts @@ -1,11 +1,10 @@ -import { ensureLogger, sleep } from "@fireproof/core-runtime"; +import { ensureLogger, hashObjectSync, sleep } from "@fireproof/core-runtime"; import { SuperThis } from "@fireproof/core-types-base"; -import { Future, KeyedResolvOnce, Logger, ResolveOnce, Result } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Lazy, Logger, ResolveOnce, Result } from "@adviser/cement"; import { FPCCProtocol, FPCCProtocolBase, FPCCEvtApp, - FPCCEvtNeedsLogin, FPCCMessage, FPCCMsgBase, FPCCReqRegisterLocalDbName, @@ -21,6 +20,8 @@ export interface PageFPCCProtocolOpts { readonly maxConnectRetries?: number; readonly iframeHref: string; readonly loginWaitTime?: number; + readonly registerWaitTime?: number; + readonly intervalMs?: number; } interface WaitForFPCCEvtApp { @@ -36,14 +37,14 @@ export class PageFPCCProtocol implements FPCCProtocol { readonly dst: string; // readonly futureConnected = new Future(); - readonly onFPCCEvtNeedsLoginFns = new Set<(msg: FPCCEvtNeedsLogin) => void>(); - readonly onFPCCEvtAppFns = new Set<(msg: FPCCEvtApp) => void>(); readonly registerFPCCEvtApp = new KeyedResolvOnce(); readonly waitforFPCCEvtAppFutures = new Map>(); waitForConnection?: ReturnType; readonly loginWaitTime: number; readonly starter = new ResolveOnce(); + readonly hash: () => string; + constructor(sthis: SuperThis, iopts: PageFPCCProtocolOpts) { const opts = { maxConnectRetries: 20, @@ -55,6 +56,7 @@ export class PageFPCCProtocol implements FPCCProtocol { this.logger = ensureLogger(sthis, "PageFPCCProtocol", { iFrameHref: this.dst, }); + this.hash = Lazy(() => hashObjectSync(opts)); this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); this.maxConnectRetries = opts.maxConnectRetries; this.loginWaitTime = opts.loginWaitTime; @@ -62,8 +64,6 @@ export class PageFPCCProtocol implements FPCCProtocol { stop(): void { this.fpccProtocol.stop(); - this.onFPCCEvtAppFns.clear(); - this.onFPCCEvtNeedsLoginFns.clear(); this.waitforFPCCEvtAppFutures.clear(); this.registerFPCCEvtApp.reset(); this.starter.reset(); @@ -73,14 +73,6 @@ export class PageFPCCProtocol implements FPCCProtocol { } } - readonly handleMessage = (_event: MessageEvent): void => { - this.fpccProtocol.handleMessage(_event); - }; - - onFPCCMessage(callback: (msg: FPCCMessage) => boolean | undefined): void { - this.fpccProtocol.onFPCCMessage(callback); - } - getAppId(): string { // setup in ready return "we-need-to-implement-app-id-this"; @@ -90,15 +82,6 @@ export class PageFPCCProtocol implements FPCCProtocol { throw new Error("Method not implemented."); }; - // readonly onceConnected = Lazy((error?: Error) => { - // if (error) { - // this.logger.Error().Err(error).Msg("Failed to connect FPCCProtocol"); - // this.futureConnected.reject(error); - // return; - // } - // this.futureConnected.resolve(); - // }); - async registerDatabase(dbName: string, ireg: Partial = {}): Promise> { return this.ready().then(() => { const sreg = { @@ -161,16 +144,25 @@ export class PageFPCCProtocol implements FPCCProtocol { }); } - injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent) => FPCCMessage): void { + injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void { this.fpccProtocol.injectSend(send); } - ready(): Promise { + sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void { + this.fpccProtocol.sendMessage(event, srcEvent); + } + + readonly ready = Lazy(async (): Promise => { return this.starter .once(async () => { await this.fpccProtocol.ready(); let maxTries = 0; const appId = this.getAppId(); + + + + + this.waitForConnection = setInterval(() => { if (maxTries > this.maxConnectRetries) { this.logger.Error().Msg("FPCC iframe connection timeout."); @@ -181,7 +173,7 @@ export class PageFPCCProtocol implements FPCCProtocol { if (maxTries && maxTries % ~~(this.maxConnectRetries / 2) === 0) { this.logger.Warn().Int("tried", maxTries).Msg("Waiting for FPCC iframe connector to be ready..."); } - this.sendMessage({ + this.fpccProtocol.sendMessage({ src: window.location.href, type: "FPCCReqWaitConnectorReady", dst: "iframe", @@ -192,12 +184,13 @@ export class PageFPCCProtocol implements FPCCProtocol { }, 100); const waitForConnectorReady = new Future(); - this.onFPCCMessage((msg: FPCCMessage): boolean | undefined => { + // this.fpccProtocol.onFPCCEvtNeedsLogin((msg: FPCCMessage): boolean | undefined => { + // this.logger.Info().Any(msg).Msg("Received needs login event from FPCC iframe"); + // this.onFPCCEvtNeedsLoginFns.forEach((cb) => cb(msg)); + // }) // console.log("PageFPCCProtocol received message", msg); - switch (true) { + // switch (true) { case isFPCCEvtNeedsLogin(msg): { - this.logger.Info().Any(msg).Msg("Received needs login event from FPCC iframe"); - this.onFPCCEvtNeedsLoginFns.forEach((cb) => cb(msg)); break; } case isFPCCEvtApp(msg): { @@ -227,15 +220,7 @@ export class PageFPCCProtocol implements FPCCProtocol { return waitForConnectorReady.asPromise(); }) .then(() => this); - } - - onFPCCEvtNeedsLogin(callback: (msg: FPCCEvtNeedsLogin) => void): void { - this.onFPCCEvtNeedsLoginFns.add(callback); - } - - onFPCCEvtApp(callback: (msg: FPCCEvtApp) => void): void { - this.onFPCCEvtAppFns.add(callback); - } + }) sendMessage(msg: FPCCSendMessage, srcEvent = new MessageEvent("sendMessage")): T { return this.fpccProtocol.sendMessage(msg, srcEvent); diff --git a/cloud/connector/page/page-handler.ts b/cloud/connector/page/page-handler.ts index 39709485a..21cb6bad6 100644 --- a/cloud/connector/page/page-handler.ts +++ b/cloud/connector/page/page-handler.ts @@ -5,7 +5,7 @@ import { Future } from "@adviser/cement"; import { Writable } from "ts-essentials"; import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; -import { FPCCMessage } from "@fireproof/cloud-connector-base"; +import { FPCCMessage, FPCCProtocolBase } from "@fireproof/cloud-connector-base"; /** * Creates an iframe element with the specified source @@ -39,12 +39,12 @@ function insertIframeAsLastElement(iframe: HTMLIFrameElement): void { /** * Main function to set up the iframe */ -export function initializeIframe(pageProtocol: PageFPCCProtocol): Promise { +export function initializeIframe(pageProtocol: FPCCProtocolBase, iframeSrc: string): Promise { (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = { FP_DEBUG: "*", }; - const iframe = createIframe(pageProtocol.dst); + const iframe = createIframe(iframeSrc); const waitForLoad = new Future(); // Add load event listener // console.log("Initializing FPCC iframe with src:", iframeHref.toString()); @@ -63,10 +63,8 @@ export function initializeIframe(pageProtocol: PageFPCCProtocol): Promise pageProtocol); + return waitForLoad.asPromise().then(() => iframe); } // Initialize when script loads diff --git a/cloud/connector/test/package.json b/cloud/connector/test/package.json index ba6d38d4c..5ac867d49 100644 --- a/cloud/connector/test/package.json +++ b/cloud/connector/test/package.json @@ -34,6 +34,7 @@ "dependencies": { "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/cloud-connector-iframe": "workspace:*", + "@fireproof/cloud-connector-svc": "workspace:*", "@fireproof/cloud-connector-page": "workspace:*", "@fireproof/core-runtime": "workspace:*", "@vitest/browser": "^4.0.4", diff --git a/cloud/connector/test/page-fpcc-protocol.test.ts b/cloud/connector/test/page-fpcc-protocol.test.ts index bf68fe245..387233183 100644 --- a/cloud/connector/test/page-fpcc-protocol.test.ts +++ b/cloud/connector/test/page-fpcc-protocol.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { PageFPCCProtocol } from "@fireproof/cloud-connector-page"; -import { IframeFPCCProtocol } from "@fireproof/cloud-connector-iframe"; +import { SvcFPCCProtocol } from "@fireproof/cloud-connector-svc"; import { FPCCMessage, FPCCPing } from "@fireproof/cloud-connector-base"; import { ensureSuperThis } from "@fireproof/core-runtime"; import { Writable } from "ts-essentials"; @@ -11,7 +11,7 @@ describe("FPCC Protocol", () => { iframeHref: "https://example.com/iframe", loginWaitTime: 1000, }); - const iframeProtocol = new IframeFPCCProtocol(sthis, { + const iframeProtocol = new SvcFPCCProtocol(sthis, { dashboardURI: "https://example.com/dashboard", cloudApiURI: "https://example.com/wait-for-token", }); diff --git a/core/gateways/cloud/to-cloud.ts b/core/gateways/cloud/to-cloud.ts index 22e6d00b4..56cf488e5 100644 --- a/core/gateways/cloud/to-cloud.ts +++ b/core/gateways/cloud/to-cloud.ts @@ -31,6 +31,7 @@ function addTenantAndLedger(opts: ToCloudOptionalOpts, uri: CoerceURI): URI { } export class SimpleTokenStrategy implements TokenStrategie { + readonly waitState: "started" | "stopped" = "stopped"; private tc: TokenAndSelectedTenantAndLedger; constructor(jwk: string) { let claims: FPCloudClaim; diff --git a/core/protocols/dashboard/msg-api.ts b/core/protocols/dashboard/msg-api.ts index 3db118e31..28e661503 100644 --- a/core/protocols/dashboard/msg-api.ts +++ b/core/protocols/dashboard/msg-api.ts @@ -8,11 +8,15 @@ import { ResTokenByResultId, } from "./msg-types.js"; import { FAPIMsgImpl } from "./msg-is.js"; +import { ensureLogger } from "@fireproof/core-runtime"; +import { SuperThis } from "@fireproof/core-types-base"; export class DashApi { readonly apiUrl: string; readonly isser = new FAPIMsgImpl(); - constructor(apiUrl: string) { + readonly logger: Logger; + constructor(sthis: SuperThis, apiUrl: string) { + this.logger = ensureLogger(sthis, "DashApi"); this.apiUrl = apiUrl; } diff --git a/core/types/protocols/cloud/gateway-control.ts b/core/types/protocols/cloud/gateway-control.ts index 161cbc239..ec6829dd9 100644 --- a/core/types/protocols/cloud/gateway-control.ts +++ b/core/types/protocols/cloud/gateway-control.ts @@ -23,6 +23,8 @@ export interface TokenAndSelectedTenantAndLedger { } export interface TokenStrategie { + waitState: "started" | "stopped"; + ready?: () => Promise; hash(): string; open(sthis: SuperThis, logger: Logger, localDbName: string, opts: ToCloudOpts): void; // tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; diff --git a/dashboard/backend/cf-serve.ts b/dashboard/backend/cf-serve.ts index 913f8f8d5..95a1897b9 100644 --- a/dashboard/backend/cf-serve.ts +++ b/dashboard/backend/cf-serve.ts @@ -35,11 +35,11 @@ export default { // return new Response(html, { status: 200, headers: { "Content-Type": "text/html" } }); // } // break; + case uri.pathname.startsWith("/@fireproof/cloud-connector-svc"): { + console.log("module request", request.method, uri.toString()); + return env.ASSETS.fetch(uri.build().appendRelative("index.js").asURL(), request as unknown as CFRequest); + } case uri.pathname.startsWith("/@fireproof/cloud-connector-iframe"): { - if (uri.pathname === "/@fireproof/cloud-connector-iframe") { - console.log("module request", request.method, uri.toString()); - return env.ASSETS.fetch(uri.build().appendRelative("index.js").asURL(), request as unknown as CFRequest); - } console.log("direct request", request.method, uri.toString()); return env.ASSETS.fetch(request as unknown as CFRequest); } diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 0e7881cbe..b7b8c9c0a 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -19,7 +19,7 @@ const serveFireproofAssets = (): Plugin => ({ // Serve the HTML file let url = req.url?.split("?")[0] || ""; if (url.startsWith("/@fireproof")) { - if (url === "/@fireproof/cloud-connector-iframe") { + if (url === "/@fireproof/cloud-connector-svc") { url += "/index.js"; } @@ -64,7 +64,7 @@ const serveFireproofAssets = (): Plugin => ({ }); const result = await esbuild.build({ - entryPoints: [path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/index.ts")], + entryPoints: [path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-svc/index.ts")], bundle: true, format: "esm", write: false, @@ -76,7 +76,7 @@ const serveFireproofAssets = (): Plugin => ({ this.emitFile({ type: "asset", - fileName: "@fireproof/cloud-connector-iframe/index.js", + fileName: "@fireproof/cloud-connector-svc/index.js", source: bundledCode, }); }, @@ -170,4 +170,4 @@ export default defineConfig({ }, }); -// console.log(">>>>>>", path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-iframe/index.ts")); +// console.log(">>>>>>", path.resolve(__dirname, "node_modules/@fireproof/cloud-connector-svc/index.ts")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d6a51ec2..bfd838b61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,8 +77,24 @@ importers: cli: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../core/runtime @@ -135,14 +151,36 @@ importers: cloud/3rd-party: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +>>>>>>> fade9b61 (wip [skip ci]) preact: specifier: ^10.27.2 version: 10.27.2 preact-render-to-string: specifier: ^6.6.2 version: 6.6.2(preact@10.27.2) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) + preact: + specifier: ^10.27.2 + version: 10.27.2 + preact-render-to-string: + specifier: ^6.6.2 + version: 6.6.2(preact@10.27.2) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) react: specifier: ^19.2.0 version: 19.2.0 @@ -169,8 +207,24 @@ importers: cloud/backend/base: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@cloudflare/workers-types': specifier: ^4.20251111.0 version: 4.20251111.0 @@ -233,8 +287,24 @@ importers: cloud/backend/cf-d1: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@cloudflare/workers-types': specifier: ^4.20251111.0 version: 4.20251111.0 @@ -294,8 +364,24 @@ importers: cloud/backend/node: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/cloud-backend-base': specifier: workspace:0.0.0 version: link:../base @@ -355,8 +441,24 @@ importers: cloud/base: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-blockstore': specifier: workspace:0.0.0 version: link:../../core/blockstore @@ -392,6 +494,7 @@ importers: specifier: ^8.8.5 version: 8.8.5 +<<<<<<< HEAD cloud/box-party: dependencies: '@adviser/cement': @@ -420,11 +523,59 @@ importers: specifier: ^7.1.12 version: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) +||||||| parent of e125fa77 (wip [skip ci]) +======= + cloud/box-party: + dependencies: + '@adviser/cement': + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) + '@fireproof/cloud-connector-svc': + specifier: workspace:* + version: link:../connector/svc + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + use-fireproof: + specifier: workspace:0.0.0 + version: link:../../use-fireproof + devDependencies: + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) + vite: + specifier: ^7.1.12 + version: 7.1.12(@types/node@24.9.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + +>>>>>>> e125fa77 (wip [skip ci]) cloud/connector/base: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.54 +<<<<<<< HEAD version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + version: 0.4.62(typescript@5.9.3) +======= + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:* version: link:../../../core/runtime @@ -447,8 +598,22 @@ importers: cloud/connector/iframe: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.54 +<<<<<<< HEAD version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + version: 0.4.62(typescript@5.9.3) +======= + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@clerk/clerk-js': specifier: ^5.102.0 version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) @@ -471,17 +636,58 @@ importers: cloud/connector/page: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.54 +<<<<<<< HEAD version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + version: 0.4.62(typescript@5.9.3) +======= + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) + '@fireproof/cloud-connector-base': + specifier: workspace:* + version: link:../base + '@fireproof/core-runtime': + specifier: workspace:* + version: link:../../../core/runtime + '@fireproof/core-types-base': + specifier: workspace:* + version: link:../../../core/types/base + ts-essentials: + specifier: ^10.1.1 + version: 10.1.1(typescript@5.9.3) + + cloud/connector/svc: + dependencies: + '@adviser/cement': + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) + '@clerk/clerk-js': + specifier: ^5.102.1 + version: 5.102.1(patch_hash=b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e)(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../base + '@fireproof/core-protocols-dashboard': + specifier: workspace:* + version: link:../../../core/protocols/dashboard '@fireproof/core-runtime': specifier: workspace:* version: link:../../../core/runtime '@fireproof/core-types-base': specifier: workspace:* version: link:../../../core/types/base + '@fireproof/core-types-protocols-cloud': + specifier: workspace:* + version: link:../../../core/types/protocols/cloud ts-essentials: specifier: ^10.1.1 version: 10.1.1(typescript@5.9.3) @@ -497,6 +703,9 @@ importers: '@fireproof/cloud-connector-page': specifier: workspace:* version: link:../page + '@fireproof/cloud-connector-svc': + specifier: workspace:* + version: link:../svc '@fireproof/core-runtime': specifier: workspace:* version: link:../../../core/runtime @@ -516,8 +725,24 @@ importers: cloud/todo-app: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/vendor': specifier: workspace:0.0.0 version: link:../../vendor @@ -547,8 +772,24 @@ importers: core/base: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-blockstore': specifier: workspace:0.0.0 version: link:../blockstore @@ -593,8 +834,24 @@ importers: core/blockstore: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../gateways/base @@ -653,8 +910,24 @@ importers: core/core: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-base': specifier: workspace:0.0.0 version: link:../base @@ -674,8 +947,24 @@ importers: core/device-id: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-keybag': specifier: workspace:0.0.0 version: link:../keybag @@ -705,8 +994,24 @@ importers: core/gateways/base: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../../runtime @@ -729,8 +1034,24 @@ importers: core/gateways/cloud: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -759,8 +1080,24 @@ importers: core/gateways/file: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -796,8 +1133,24 @@ importers: core/gateways/file-deno: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../../types/base @@ -814,8 +1167,24 @@ importers: core/gateways/file-node: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../../types/base @@ -826,8 +1195,24 @@ importers: core/gateways/indexeddb: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -850,8 +1235,24 @@ importers: core/gateways/memory: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -881,8 +1282,24 @@ importers: core/keybag: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-file': specifier: workspace:0.0.0 version: link:../gateways/file @@ -911,8 +1328,24 @@ importers: core/protocols/cloud: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../../runtime @@ -935,8 +1368,24 @@ importers: core/protocols/dashboard: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../../runtime @@ -953,8 +1402,24 @@ importers: core/runtime: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@adviser/ts-xxhash': specifier: ^1.0.2 version: 1.0.2 @@ -990,8 +1455,24 @@ importers: core/tests: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core': specifier: workspace:0.0.0 version: link:../core @@ -1086,6 +1567,9 @@ importers: playwright-chromium: specifier: ^1.56.1 version: 1.56.1 + playwright-core: + specifier: ^1.56.1 + version: 1.56.1 vitest: specifier: ^4.0.8 version: 4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) @@ -1096,8 +1580,24 @@ importers: core/types/base: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-blockstore': specifier: workspace:0.0.0 version: link:../blockstore @@ -1123,8 +1623,24 @@ importers: core/types/blockstore: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../base @@ -1148,8 +1664,24 @@ importers: core/types/protocols/cloud: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../../base @@ -1176,8 +1708,24 @@ importers: core/types/runtime: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/vendor': specifier: workspace:0.0.0 version: link:../../../vendor @@ -1188,8 +1736,24 @@ importers: dashboard: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@clerk/backend': specifier: ^2.21.0 version: 2.21.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -1284,6 +1848,9 @@ importers: '@fireproof/cloud-connector-iframe': specifier: workspace:0.0.0 version: link:../cloud/connector/iframe + '@fireproof/cloud-connector-svc': + specifier: workspace:0.0.0 + version: link:../cloud/connector/svc '@fireproof/core-cli': specifier: workspace:* version: link:../cli @@ -1363,8 +1930,24 @@ importers: use-fireproof: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../cloud/connector/base @@ -1448,8 +2031,24 @@ importers: vendor: dependencies: '@adviser/cement': +<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) +||||||| parent of fade9b61 (wip [skip ci]) + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +======= +<<<<<<< HEAD + specifier: ^0.4.58 + version: 0.4.62(typescript@5.9.3) +||||||| parent of e125fa77 (wip [skip ci]) + specifier: ^0.4.54 + version: 0.4.54(typescript@5.9.3) +======= + specifier: ^0.4.58 + version: 0.4.58(typescript@5.9.3) +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) yocto-queue: specifier: ^1.2.2 version: 1.2.2 @@ -1481,8 +2080,24 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} +<<<<<<< HEAD '@adviser/cement@0.4.63': resolution: {integrity: sha512-ctOtXLi959Ay+lO5fES55ejiNV7EHR0v9GCzCWzdbYlNOh4W165dHpplGoxsrZ2hxzKtQGIk8Qu66YOD+76mhg==} +||||||| parent of fade9b61 (wip [skip ci]) + '@adviser/cement@0.4.62': + resolution: {integrity: sha512-EwfhbibpB6eKXYw8h8CWBueZ44mIOlll08poUBkKsABTqEXbv54dflQAWX5lnRA/FNRD2ThgejWcxPKxvEvacg==} +======= +<<<<<<< HEAD + '@adviser/cement@0.4.62': + resolution: {integrity: sha512-EwfhbibpB6eKXYw8h8CWBueZ44mIOlll08poUBkKsABTqEXbv54dflQAWX5lnRA/FNRD2ThgejWcxPKxvEvacg==} +||||||| parent of e125fa77 (wip [skip ci]) + '@adviser/cement@0.4.54': + resolution: {integrity: sha512-LTJQqGgBb6A3pNVLJFIX2/cnkOt9WPakPFDZXQu8Jpza+ZqapqOo4rCS852ZtchjvorKwRwwQIku4OB/bbVcEg==} +======= + '@adviser/cement@0.4.58': + resolution: {integrity: sha512-w0mRru1fZLgdXPhlwfQs2woPQwXqmHwW20KX/gWBX6e+ftMHAEJcAwIzPC5tPGJXb2bCUjf3XHU17HjlD4e1/Q==} +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) engines: {node: '>=20.19.0'} hasBin: true @@ -6278,7 +6893,19 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} +<<<<<<< HEAD '@adviser/cement@0.4.63(typescript@5.9.3)': +||||||| parent of fade9b61 (wip [skip ci]) + '@adviser/cement@0.4.62(typescript@5.9.3)': +======= +<<<<<<< HEAD + '@adviser/cement@0.4.62(typescript@5.9.3)': +||||||| parent of e125fa77 (wip [skip ci]) + '@adviser/cement@0.4.54(typescript@5.9.3)': +======= + '@adviser/cement@0.4.58(typescript@5.9.3)': +>>>>>>> e125fa77 (wip [skip ci]) +>>>>>>> fade9b61 (wip [skip ci]) dependencies: ts-essentials: 10.1.1(typescript@5.9.3) yaml: 2.8.1 diff --git a/use-fireproof/fp-cloud-connect-strategy-impl.ts b/use-fireproof/fp-cloud-connect-strategy-impl.ts new file mode 100644 index 000000000..70d0f8592 --- /dev/null +++ b/use-fireproof/fp-cloud-connect-strategy-impl.ts @@ -0,0 +1,166 @@ +import { BuildURI, KeyedResolvOnce, Lazy, Logger, ResolveSeq, Result } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core-types-base"; +import { ToCloudOpts, TokenAndSelectedTenantAndLedger, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; +import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; + +import { FPCCEvtApp, dbAppKey } from "@fireproof/cloud-connector-base"; +import { FPCloudConnectOpts, PageControllerImpl } from "./fp-cloud-connect-strategy.js"; +import { FPCloudFrontendImpl } from "./window-open-fp-cloud.js"; + + +const registerLocalDbNames = new KeyedResolvOnce, string>(); + +export class FPCloudConnectStrategyImpl implements TokenStrategie { + overlayNode?: HTMLDivElement; + waitState: "started" | "stopped" = "stopped"; + + readonly fpCloudConnectURL: string; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly pageController: PageControllerImpl; + + constructor(opts: Partial) { + + const dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/"; + let fpCloudConnectURL: BuildURI; + if (opts.fpCloudConnectURL) { + fpCloudConnectURL = BuildURI.from(opts.fpCloudConnectURL); + } else { + fpCloudConnectURL = BuildURI.from( + // eslint-disable-next-line no-restricted-globals + new URL("/", dashboardURI).toString(), + ).pathname("/@fireproof/cloud-connector-iframe/injected-iframe.html"); + } + + if (opts.dashboardURI) { + fpCloudConnectURL.setParam("dashboard_uri", opts.dashboardURI); + } + if (opts.cloudApiURI) { + fpCloudConnectURL.setParam("cloud_api_uri", opts.cloudApiURI); + } + this.fpCloudConnectURL = fpCloudConnectURL.toString(); + // console.log("FPCloudConnectStrategy constructed with fpCloudConnectURL", this.fpCloudConnectURL); + this.sthis = opts.sthis ?? ensureSuperThis(); + this.logger = ensureLogger(this.sthis, "FPCloudConnectStrategy"); + this.pageController = new PageControllerImpl({ + sthis: this.sthis, + iframeHref: this.fpCloudConnectURL, + logger: this.logger, + frontend: opts.frontend ?? new FPCloudFrontendImpl(opts), + }); + } + readonly hash = Lazy(() => + hashObjectSync({ + pageController: this.pageController.hash(), + fpCloudConnectURL: this.fpCloudConnectURL, + }), + ); + + readonly openloginSeq = new ResolveSeq(); + // readonly waitForTokenPerLocalDbFuture = new KeyedResolvOnce>>(); + + fpccEvtApp2TokenAndClaims(evt: FPCCEvtApp): Result { + // convertToTokenAndClaims({ + + // }, this.logger, evt.localDb.accessToken) + const tAndC: TokenAndSelectedTenantAndLedger = { + token: evt.localDb.accessToken, + claims: { + selected: { + tenant: evt.localDb.tenantId, + ledger: evt.localDb.ledgerId, + }, + }, + }; + return Result.Ok(tAndC); + } + + // getPageProtocol(sthis: SuperThis): Promise { + // const key = ppageProtocolKey(this.fpCloudConnectURL); + // return ppageProtocolInstances.get(key).once(async () => { + // console.log("FPCloudConnectStrategy creating new PageFPCCProtocol for key", key, import.meta.url); + // const ppage = new PageFPCCProtocol(sthis, { iframeHref: key }); + // await initializeIframe(ppage); + // await ppage.ready(); + // ppage.onFPCCEvtNeedsLogin((msg) => { + // this.openloginSeq.add(() => { + // // test if all dbs are ready + // console.log("FPCloudConnectStrategy detected needs login event"); + // this.openFireproofLogin(msg); + // return sleep(10000); + // }); + // // logger.Info().Msg("FPCloudConnectStrategy detected needs login event"); + // }); + // // this.waitForTokenPerLocalDbFuture.get(key).once(() => new Future()); + // ppage.onFPCCEvtApp((evt) => { + // const key = dbAppKey({ appId: evt.appId, dbName: evt.localDb.dbName }); + // const rTAndC = this.fpccEvtApp2TokenAndClaims(evt); + // console.log("FPCloudConnectStrategy received FPCCEvtApp, resolving waitForTokenAndClaims for key", key, rTAndC.Ok()); + // this.waitForTokenAndClaims.get(key).reset(() => rTAndC); + // // if (future) { + // // future.resolve(rTAndC) + // // } + // }); + // return ppage.ready(); + // }); + // } + + open(sthis: SuperThis, _logger: Logger, localDbName: string, _opts: ToCloudOpts) { + console.log("FPCloudConnectStrategy open called for localDbName", localDbName); + return this.pageController.ready().then(() => { + return registerLocalDbNames.get(`${localDbName}:${this.pageController.appId()}:${ppage.dst}`).once(() => { + console.log("FPCloudConnectStrategy open registering localDbName", localDbName); + }); + }); + } + + // private currentToken?: TokenAndClaims; + + // waiting?: ReturnType; + + stop() { + console.log("FPCloudConnectStrategy stop called"); + // if (this.waiting) { + // clearTimeout(this.waiting); + // this.waiting = undefined; + // } + // this.waitState = "stopped"; + } + + readonly waitForTokenAndClaims = new KeyedResolvOnce>(); + // async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { + // console.log("FPCloudConnectStrategy tryToken called", opts); + // // if (!this.currentToken) { + // // const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; + // // this.currentToken = await webCtx.token(); + // // // console.log("RedirectStrategy tryToken - ctx", this.currentToken); + // // } + // // return this.currentToken; + // return undefined; + // } + + async waitForToken( + _sthis: SuperThis, + _logger: Logger, + localDbName: string, + _opts: ToCloudOpts, + ): Promise> { + // console.log("FPCloudConnectStrategy waitForToken called for localDbName", localDbName); + await this.pageController.ready(); + const key = dbAppKey({ appId: this.pageController.appId(), dbName: localDbName }); + await this.openloginSeq.flush(); + return this.waitForTokenAndClaims.get(key).once(() => { + return this.pageController.registerDatabase(localDbName).then((evt) => { + if (evt.isErr()) { + console.log("FPCloudConnectStrategy waitForToken registering database failed for key", key, evt); + return Result.Err(evt); + } + console.log("FPCloudConnectStrategy waitForToken resolving for key", key); + return this.fpccEvtApp2TokenAndClaims(evt.Ok()); + }); + + // const future = this.waitForTokenPerLocalDbFuture.get(key).once(() => new Future>()) + // return future.asPromise(); + }); + } +} diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index 6ba19fd2b..b580faa4b 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,251 +1,259 @@ -import { BuildURI, KeyedResolvOnce, Lazy, Logger, ResolveSeq, Result, URI } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Lazy, Logger, poller, ResolveSeq } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; -import { ToCloudOpts, TokenAndSelectedTenantAndLedger, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { ensureLogger, ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; +import { TokenStrategie } from "@fireproof/core-types-protocols-cloud"; +import { ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; -import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; -import { PageFPCCProtocol, initializeIframe } from "@fireproof/cloud-connector-page"; -import { FPCCEvtApp, FPCCEvtNeedsLogin, dbAppKey } from "@fireproof/cloud-connector-base"; -import DOMPurify from "dompurify"; +import { FPCCProtocol, FPCCProtocolBase, isInIframe } from "@fireproof/cloud-connector-base"; +import { useEffect, useState } from "react"; +import { defaultFPCloudConnectorOpts, fpCloudConnector } from "../cloud/connector/svc/fp-cloud-connector.js"; +import { FPCloudConnectStrategyImpl } from "./fp-cloud-connect-strategy-impl.js"; +import { initializeIframe, PageFPCCProtocolOpts } from "@fireproof/cloud-connector-page"; +import { FPCloudFrontend, FPCloudFrontendImpl} from "./window-open-fp-cloud.js"; export interface FPCloudConnectOpts extends RedirectStrategyOpts { readonly dashboardURI?: string; readonly cloudApiURI?: string; readonly fpCloudConnectURL: string; + readonly pageController: PageControllerImpl; readonly title?: string; readonly sthis?: SuperThis; + readonly frontend?: FPCloudFrontend; } -// open(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): void; -// tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; -// waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise; -// stop(): void; +// which cases exist +// stategy is called in the calling page +// - ask my self if providing iframe services use them +// - search for iframes and ask if ready use them +// - if not found create an iframe and wait for it to be ready -const ppageProtocolInstances = new KeyedResolvOnce(); - -function ppageProtocolKey(iframeSrc: string): string { - let iframeHref: URI; - if (typeof iframeSrc === "string" && iframeSrc.match(/^[./]/)) { - // Infer the path to in-iframe.js from the current module's location - // eslint-disable-next-line no-restricted-globals - const scriptUrl = new URL(import.meta.url); - // eslint-disable-next-line no-restricted-globals - iframeHref = URI.from(new URL(iframeSrc, scriptUrl).href); - } else { - iframeHref = URI.from(iframeSrc); - } - return iframeHref.toString(); +// stategy is called in an iframe +// - ask my self if providing iframe services use them +// - wait for parent to provide services + +interface PageControllerOpts { + readonly window: Window; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly frontend: FPCloudFrontend; } -const registerLocalDbNames = new KeyedResolvOnce, string>(); +export type PageControllerImplOpts = PageFPCCProtocolOpts & Partial; -export class FPCloudConnectStrategy implements TokenStrategie { - overlayNode?: HTMLDivElement; - waitState: "started" | "stopped" = "stopped"; +// const registerIframe = Lazy((callback: (iframe: HTMLIFrameElement) => void) => { +// // Check existing iframes first +// document.querySelectorAll("iframe").forEach((iframe) => { +// callback(iframe as HTMLIFrameElement); +// }); - readonly overlayCss: string; - readonly overlayHtml: (redirectLink: string) => string; - readonly title: string; - readonly fpCloudConnectURL: string; - readonly sthis: SuperThis; - readonly logger: Logger; +// // Watch for new iframes +// const observer = new MutationObserver((mutations) => { +// mutations.forEach((mutation) => { +// mutation.addedNodes.forEach((node) => { +// if (node.nodeName === "IFRAME") { +// callback(node as HTMLIFrameElement); +// } - constructor(opts: Partial = {}) { - this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); - this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; +// // Check if added node contains iframes +// if (node instanceof Element) { +// node.querySelectorAll("iframe").forEach((iframe) => { +// callback(iframe as HTMLIFrameElement); +// }); +// } +// }); +// }); +// }); - const dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/"; - let fpCloudConnectURL: BuildURI; - if (opts.fpCloudConnectURL) { - fpCloudConnectURL = BuildURI.from(opts.fpCloudConnectURL); - } else { - fpCloudConnectURL = BuildURI.from( - // eslint-disable-next-line no-restricted-globals - new URL("/", dashboardURI).toString(), - ).pathname("/@fireproof/cloud-connector-iframe/injected-iframe.html"); - } +// observer.observe(document.body, { +// childList: true, +// subtree: true, +// }); - if (opts.dashboardURI) { - fpCloudConnectURL.setParam("dashboard_uri", opts.dashboardURI); - } - if (opts.cloudApiURI) { - fpCloudConnectURL.setParam("cloud_api_uri", opts.cloudApiURI); - } - this.fpCloudConnectURL = fpCloudConnectURL.toString(); - // console.log("FPCloudConnectStrategy constructed with fpCloudConnectURL", this.fpCloudConnectURL); - this.title = opts.title ?? "Fireproof Login"; +// return observer; // Return so you can disconnect later +// }); + +export class PageControllerImpl { + mode: "myself" | "parent" | "child" | "unknown" | "timeout" = "unknown"; + + readonly window: Window; + readonly registerWaitTime: number; + readonly protocol: FPCCProtocolBase; + readonly sthis: SuperThis; + readonly intervalMs: number; + readonly iframeHref: string; + readonly frontend: FPCloudFrontend; + + constructor(opts: PageControllerImplOpts) { + this.window = opts.window ?? window; this.sthis = opts.sthis ?? ensureSuperThis(); - this.logger = ensureLogger(this.sthis, "FPCloudConnectStrategy"); + this.registerWaitTime = opts.registerWaitTime || 10000; + this.intervalMs = opts.intervalMs || 150; + this.protocol = new FPCCProtocolBase(this.sthis, opts.logger); + this.iframeHref = opts.iframeHref; + this.frontend = opts.frontend ?? new FPCloudFrontendImpl({ + sthis: this.sthis, + }); } - readonly hash = Lazy(() => + + hash = Lazy(() => hashObjectSync({ - overlayCss: this.overlayCss, - overlayHtml: this.overlayHtml("X").toString(), - fpCloudConnectURL: this.fpCloudConnectURL, + registerWaitTime: this.registerWaitTime, + intervalMs: this.intervalMs, + protocol: this.protocol.hash(), + iframeHref: this.iframeHref, }), ); - openFireproofLogin(msg: FPCCEvtNeedsLogin): void { - // const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; - this.logger.Debug().Url(msg.loginURL).Msg("open redirect"); - - let overlayNode = document.body.querySelector("#fpOverlay") as HTMLDivElement; - if (!overlayNode) { - const styleNode = document.createElement("style"); - styleNode.innerHTML = DOMPurify.sanitize(this.overlayCss); - document.head.appendChild(styleNode); - overlayNode = document.createElement("div") as HTMLDivElement; - overlayNode.id = "fpOverlay"; - overlayNode.className = "fpOverlay"; - const myHtml = this.overlayHtml(msg.loginURL); - console.log("FPCloudConnectStrategy openFireproofLogin creating overlay with html", myHtml); - overlayNode.innerHTML = DOMPurify.sanitize(myHtml); - document.body.appendChild(overlayNode); - overlayNode.querySelector(".fpCloseButton")?.addEventListener("click", () => { - if (overlayNode) { - if (overlayNode.style.display === "block") { - overlayNode.style.display = "none"; - this.stop(); - } else { - overlayNode.style.display = "block"; - } - } - }); - } - overlayNode.style.display = "block"; - this.overlayNode = overlayNode; - const width = 800; - const height = 600; - const parentScreenX = window.screenX || window.screenLeft; // Cross-browser compatibility - const parentScreenY = window.screenY || window.screenTop; // Cross-browser compatibility - - // Get the parent window's outer dimensions (including chrome) - const parentOuterWidth = window.outerWidth; - const parentOuterHeight = window.outerHeight; - - // Calculate the left position for the new window - // Midpoint of parent window's width - half of new window's width - const left = parentScreenX + parentOuterWidth / 2 - width / 2; - - // Calculate the top position for the new window - // Midpoint of parent window's height - half of new window's height - const top = parentScreenY + parentOuterHeight / 2 - height / 2; - - window.open( - // eslint-disable-next-line no-restricted-globals - new URL(msg.loginURL), - this.title, - `left=${left},top=${top},width=${width},height=${height},scrollbars=yes,resizable=yes,popup=yes`, - ); - // window.location.href = url.toString(); - } + readonly appId = Lazy(() => { + // setup in ready + return `we-need-to-implement-app-id-this:${this.sthis.nextId(8)}`; + }); readonly openloginSeq = new ResolveSeq(); - // readonly waitForTokenPerLocalDbFuture = new KeyedResolvOnce>>(); - - fpccEvtApp2TokenAndClaims(evt: FPCCEvtApp): Result { - // convertToTokenAndClaims({ - - // }, this.logger, evt.localDb.accessToken) - const tAndC: TokenAndSelectedTenantAndLedger = { - token: evt.localDb.accessToken, - claims: { - selected: { - tenant: evt.localDb.tenantId, - ledger: evt.localDb.ledgerId, - }, - }, - }; - return Result.Ok(tAndC); - } + readonly ready = Lazy(async (): Promise => { + const actions: Promise<"timeout" | "parent" | "myself" | "child">[] = [ + sleep(this.registerWaitTime).then(() => "timeout" as const), + ]; + actions.push(this.myselfWaiting().then(() => "myself" as const)); + if (isInIframe()) { + actions.push(this.parentWaiting().then(() => "parent" as const)); + } else { + actions.push(this.childWaiting().then(() => "child" as const)); + } - getPageProtocol(sthis: SuperThis): Promise { - const key = ppageProtocolKey(this.fpCloudConnectURL); - return ppageProtocolInstances.get(key).once(async () => { - console.log("FPCloudConnectStrategy creating new PageFPCCProtocol for key", key, import.meta.url); - const ppage = new PageFPCCProtocol(sthis, { iframeHref: key }); - await initializeIframe(ppage); - await ppage.ready(); - ppage.onFPCCEvtNeedsLogin((msg) => { - this.openloginSeq.add(() => { - // test if all dbs are ready - console.log("FPCloudConnectStrategy detected needs login event"); - this.openFireproofLogin(msg); - return sleep(10000); - }); - // logger.Info().Msg("FPCloudConnectStrategy detected needs login event"); - }); - // this.waitForTokenPerLocalDbFuture.get(key).once(() => new Future()); - ppage.onFPCCEvtApp((evt) => { - const key = dbAppKey({ appId: evt.appId, dbName: evt.localDb.dbName }); - const rTAndC = this.fpccEvtApp2TokenAndClaims(evt); - console.log("FPCloudConnectStrategy received FPCCEvtApp, resolving waitForTokenAndClaims for key", key, rTAndC.Ok()); - this.waitForTokenAndClaims.get(key).reset(() => rTAndC); - // if (future) { - // future.resolve(rTAndC) - // } + this.protocol.onFPCCEvtNeedsLogin((msg) => { + this.openloginSeq.add(async () => { + // test if all dbs are ready + console.log("FPCloudConnectStrategy detected needs login event"); + this.frontend.openFireproofLogin(msg); + return; }); - return ppage.ready(); + // logger.Info().Msg("FPCloudConnectStrategy detected needs login event"); }); - } - open(sthis: SuperThis, logger: Logger, localDbName: string, _opts: ToCloudOpts) { - console.log("FPCloudConnectStrategy open called for localDbName", localDbName); - return this.getPageProtocol(sthis).then((ppage) => { - return registerLocalDbNames.get(`${localDbName}:${ppage.getAppId()}:${ppage.dst}`).once(() => { - console.log("FPCloudConnectStrategy open registering localDbName", localDbName); - }); + return Promise.race(actions).then((mode) => { + this.mode = mode; }); - } + }); - // private currentToken?: TokenAndClaims; + - // waiting?: ReturnType; + async #waitingForReady(tid: string, dst: string): Promise { + const ready = new Future(); + const unreg = this.protocol.onFPCCEvtConnectorReady((msg, _srcEvent) => { + if (msg.tid === tid) { + return Promise.resolve(); + } + ready.resolve(); + }); - stop() { - console.log("FPCloudConnectStrategy stop called"); - // if (this.waiting) { - // clearTimeout(this.waiting); - // this.waiting = undefined; - // } - // this.waitState = "stopped"; + const abCtl = new AbortController(); + return Promise.race([ + poller( + async () => { + this.protocol.sendMessage( + { + tid: tid, + src: "parentWaiting", + dst: "myself", + type: "FPCCReqWaitConnectorReady", + }, + dst, + ); + return { + state: "waiting", + }; + }, + { + intervalMs: this.intervalMs, + abortSignal: abCtl.signal, + }, + ).then(() => { + /* nop */ + }), + ready.asPromise(), + ]).finally(() => { + unreg(); + abCtl.abort(); + }); } - - readonly waitForTokenAndClaims = new KeyedResolvOnce>(); - // async tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise { - // console.log("FPCloudConnectStrategy tryToken called", opts); - // // if (!this.currentToken) { - // // const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; - // // this.currentToken = await webCtx.token(); - // // // console.log("RedirectStrategy tryToken - ctx", this.currentToken); - // // } - // // return this.currentToken; - // return undefined; - // } - - async waitForToken( - _sthis: SuperThis, - _logger: Logger, - localDbName: string, - _opts: ToCloudOpts, - ): Promise> { - // console.log("FPCloudConnectStrategy waitForToken called for localDbName", localDbName); - const ppage = await this.getPageProtocol(this.sthis); - const key = dbAppKey({ appId: ppage.getAppId(), dbName: localDbName }); - await this.openloginSeq.flush(); - return this.waitForTokenAndClaims.get(key).once(() => { - return ppage.registerDatabase(localDbName).then((evt) => { - if (evt.isErr()) { - console.log("FPCloudConnectStrategy waitForToken registering database failed for key", key, evt); - return Result.Err(evt); - } - console.log("FPCloudConnectStrategy waitForToken resolving for key", key); - return this.fpccEvtApp2TokenAndClaims(evt.Ok()); + async parentWaiting(): Promise { + const tid = this.sthis.nextId(16).str; + await this.#waitingForReady(tid, this.window.parent.location.href); + } + async childWaiting(): Promise { + const tid = this.sthis.nextId(16).str; + const waitingIframes: Promise[] = []; + const iframes = document.querySelectorAll("iframe"); + if (iframes.length === 0) { + const iframe = await initializeIframe(this.protocol, this.iframeHref); + waitingIframes.push(this.#waitingForReady(tid, iframe.src)); + } else { + iframes.forEach((iframe) => { + waitingIframes.push(this.#waitingForReady(tid, (iframe as HTMLIFrameElement).src)); }); + } + return Promise.race(waitingIframes); + } + async myselfWaiting(): Promise { + const tid = this.sthis.nextId(16).str; + await this.#waitingForReady(tid, this.window.location.href); + } +} + +export const PageController = Lazy((opts: PageControllerImplOpts) => { + return new PageControllerImpl(opts); +}); - // const future = this.waitForTokenPerLocalDbFuture.get(key).once(() => new Future>()) - // return future.asPromise(); +// open(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): void; +// tryToken(sthis: SuperThis, logger: Logger, opts: ToCloudOpts): Promise; +// waitForToken(sthis: SuperThis, logger: Logger, deviceId: string, opts: ToCloudOpts): Promise; +// stop(): void; + +// const ppageProtocolInstances = new KeyedResolvOnce(); + +// function ppageProtocolKey(iframeSrc: string): string { +// let iframeHref: URI; +// if (typeof iframeSrc === "string" && iframeSrc.match(/^[./]/)) { +// // Infer the path to in-iframe.js from the current module's location +// // eslint-disable-next-line no-restricted-globals +// const scriptUrl = new URL(import.meta.url); +// // eslint-disable-next-line no-restricted-globals +// iframeHref = URI.from(new URL(iframeSrc, scriptUrl).href); +// } else { +// iframeHref = URI.from(iframeSrc); +// } +// return iframeHref.toString(); +// } + +const fpCloudConnectStrategyInstances = new KeyedResolvOnce(); +// this is the frontend of fp service connector +export function FPCloudConnectStrategy(opts: Partial = {}): TokenStrategie { + const key = hashObjectSync(opts); + return fpCloudConnectStrategyInstances.get(key).once(() => { + return new FPCloudConnectStrategyImpl(opts); + }); +} + + +// this is the backend fp service connector +export function useFPCloudConnectSvc(): { fpSvc: FPCCProtocol; state: string } { + const fpSvc = fpCloudConnector( + defaultFPCloudConnectorOpts({ + loadUrlStr: window.location.href, + }), + ); + const [fpSvcState, setFpSvcState] = useState("initializing"); + useEffect(() => { + console.log("useFPCloudConnect initializing token strategy"); + fpSvc.ready().then(() => { + console.log("useFPCloudConnect token strategy ready"); + setFpSvcState("ready"); }); - } + }, [fpSvc]); + + return { + fpSvc, + state: fpSvcState, + }; } diff --git a/use-fireproof/iframe-fp-cloud-connect-strategy.ts b/use-fireproof/iframe-fp-cloud-connect-strategy.ts new file mode 100644 index 000000000..27c64444e --- /dev/null +++ b/use-fireproof/iframe-fp-cloud-connect-strategy.ts @@ -0,0 +1,74 @@ +import { Lazy, Logger, Result } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core-types-base"; +import { ToCloudOpts, TokenAndSelectedTenantAndLedger, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; +import { hashObjectSync } from "@fireproof/core-runtime"; + +import { defaultFPCloudConnectorOpts, fpCloudConnector } from "../cloud/connector/svc/fp-cloud-connector.js"; +import { SvcFPCCProtocol } from "../cloud/connector/svc/index.js"; + +class IframeFPCloudConnectStrategy implements TokenStrategie { + waitState: "started" | "stopped" = "stopped"; + readonly svc: SvcFPCCProtocol; + readonly hash: () => string; + + constructor(opts: Partial = {}) { + this.svc = fpCloudConnector(defaultFPCloudConnectorOpts(opts)); + this.hash = Lazy(() => hashObjectSync(opts)); + } + + readonly waitForIframes = Lazy((callback: (iframe: HTMLIFrameElement) => void) => { + // Check existing iframes first + document.querySelectorAll("iframe").forEach((iframe) => { + callback(iframe as HTMLIFrameElement); + }); + + // Watch for new iframes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeName === "IFRAME") { + callback(node as HTMLIFrameElement); + } + + // Check if added node contains iframes + if (node instanceof Element) { + node.querySelectorAll("iframe").forEach((iframe) => { + callback(iframe as HTMLIFrameElement); + }); + } + }); + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + return observer; // Return so you can disconnect later + }); + + ready(): Promise { + return this.svc.ready().then(() => { + this.waitForIframes((iframe) => { + this.svc.serveIframe(iframe); + }); + this.waitState = "started"; + }); + } + + open(_sthis: SuperThis, _logger: Logger, _localDbName: string, _opts: ToCloudOpts): void { + throw new Error("Method not implemented."); + } + waitForToken( + _sthis: SuperThis, + _logger: Logger, + _localDbName: string, + _opts: ToCloudOpts, + ): Promise> { + throw new Error("Method not implemented."); + } + stop(): void { + throw new Error("Method not implemented."); + } +} diff --git a/use-fireproof/iframe-strategy.ts b/use-fireproof/iframe-strategy.ts index fe0289035..b738aaeb7 100644 --- a/use-fireproof/iframe-strategy.ts +++ b/use-fireproof/iframe-strategy.ts @@ -5,6 +5,7 @@ import { WebCtx } from "./react/use-attach.js"; import { WebToCloudCtx } from "./react/types.js"; export class IframeStrategy implements TokenStrategie { + waitState: "started" | "stopped" = "stopped"; hash(): string { return "IframeStrategy: this is not a advance"; } diff --git a/use-fireproof/index.ts b/use-fireproof/index.ts index 814cb1358..a4296cc69 100644 --- a/use-fireproof/index.ts +++ b/use-fireproof/index.ts @@ -22,6 +22,8 @@ import { ensureSuperThis } from "@fireproof/core-runtime"; export { FPCloudConnectStrategy } from "./fp-cloud-connect-strategy.js"; export { convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; +export * as jsx from "./jsx-helper.js"; + export type UseFpToCloudParam = Omit, "context">, "events"> & Partial & { readonly strategy?: TokenStrategie; diff --git a/use-fireproof/redirect-strategy.ts b/use-fireproof/redirect-strategy.ts index 30afb1add..48994f726 100644 --- a/use-fireproof/redirect-strategy.ts +++ b/use-fireproof/redirect-strategy.ts @@ -162,7 +162,7 @@ export class RedirectStrategy implements TokenStrategie { throw new Error("waitForToken not working on redirect strategy"); } const webCtx = opts.context.get(WebCtx) as WebToCloudCtx; - const dashApi = new DashApi(webCtx.tokenApiURI); + const dashApi = new DashApi(sthis, webCtx.tokenApiURI); this.waitState = "started"; return new Promise>((resolve) => { this.getTokenAndClaimsByResultId(logger, dashApi, this.resultId, opts, (tokenAndClaims) => { diff --git a/use-fireproof/window-open-fp-cloud.ts b/use-fireproof/window-open-fp-cloud.ts new file mode 100644 index 000000000..3dac09c99 --- /dev/null +++ b/use-fireproof/window-open-fp-cloud.ts @@ -0,0 +1,106 @@ +import { FPCCEvtNeedsLogin } from "@fireproof/cloud-connector-base"; +import DOMPurify from "dompurify"; +import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; +import { Logger } from "@adviser/cement"; +import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; +import { SuperThis } from "@fireproof/core-types-base"; +import { RedirectStrategyOpts } from "./redirect-strategy.js"; + +export interface FPCloudFrontend { + hash(): string; + openFireproofLogin(msg: FPCCEvtNeedsLogin): void; + stop(): void; +} + +export interface FPCloudFrontendImplOpts extends RedirectStrategyOpts { + readonly sthis: SuperThis; + readonly title: string; +} + +export class FPCloudFrontendImpl implements FPCloudFrontend { + readonly logger: Logger; + readonly sthis: SuperThis; + readonly overlayCss: string; + readonly overlayHtml: (redirectLink: string) => string; + readonly title: string; + + overlayNode?: HTMLDivElement; + + constructor(readonly opts: Partial = {}) { + this.sthis = opts.sthis ?? ensureSuperThis(); + this.logger = ensureLogger(this.sthis, "FPCloudFrontendImpl"); + + this.overlayCss = opts.overlayCss ?? defaultOverlayCss(); + this.overlayHtml = opts.overlayHtml ?? defaultOverlayHtml; + this.title = opts.title ?? "Fireproof Login"; + } + + hash(): string { + return hashObjectSync({ + overlayCss: this.overlayCss, + overlayHtml: this.overlayHtml(""), + title: this.title, + }); + } + + stop(): void { + this.logger.Debug().Msg("stop called"); + this.overlayNode?.remove(); + this.overlayNode = undefined; + } + + openFireproofLogin(msg: FPCCEvtNeedsLogin): void { + // const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; + this.logger.Debug().Url(msg.loginURL).Msg("open redirect"); + + let overlayNode = document.body.querySelector("#fpOverlay") as HTMLDivElement; + if (!overlayNode) { + const styleNode = document.createElement("style"); + styleNode.innerHTML = DOMPurify.sanitize(this.overlayCss); + document.head.appendChild(styleNode); + overlayNode = document.createElement("div") as HTMLDivElement; + overlayNode.id = "fpOverlay"; + overlayNode.className = "fpOverlay"; + const myHtml = this.overlayHtml(msg.loginURL); + console.log("FPCloudConnectStrategy openFireproofLogin creating overlay with html", myHtml); + overlayNode.innerHTML = DOMPurify.sanitize(myHtml); + document.body.appendChild(overlayNode); + overlayNode.querySelector(".fpCloseButton")?.addEventListener("click", () => { + if (overlayNode) { + if (overlayNode.style.display === "block") { + overlayNode.style.display = "none"; + this.stop(); + } else { + overlayNode.style.display = "block"; + } + } + }); + } + overlayNode.style.display = "block"; + this.overlayNode = overlayNode; + const width = 800; + const height = 600; + const parentScreenX = window.screenX || window.screenLeft; // Cross-browser compatibility + const parentScreenY = window.screenY || window.screenTop; // Cross-browser compatibility + + // Get the parent window's outer dimensions (including chrome) + const parentOuterWidth = window.outerWidth; + const parentOuterHeight = window.outerHeight; + + // Calculate the left position for the new window + // Midpoint of parent window's width - half of new window's width + const left = parentScreenX + parentOuterWidth / 2 - width / 2; + + // Calculate the top position for the new window + // Midpoint of parent window's height - half of new window's height + const top = parentScreenY + parentOuterHeight / 2 - height / 2; + + window.open( + // eslint-disable-next-line no-restricted-globals + new URL(msg.loginURL), + this.title, + `left=${left},top=${top},width=${width},height=${height},scrollbars=yes,resizable=yes,popup=yes`, + ); + + } +} From d748ebc187ebb51f0852ea4a06e0bba4be105fe5 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 10 Nov 2025 12:08:40 +0100 Subject: [PATCH 22/23] wip --- cloud/backend/base/ws-sockets.test.ts | 4 +- cloud/connector/base/fpcc-protocol.ts | 30 +- cloud/connector/base/package.json | 2 +- cloud/connector/iframe/package.json | 2 +- cloud/connector/page/package.json | 2 +- cloud/connector/page/page-fpcc-protocol.ts | 341 ++++++---- cloud/connector/page/page-handler.ts | 4 +- cloud/connector/svc/clerk-fpcc-evt-entity.ts | 208 ++++++ cloud/connector/svc/fp-cloud-connector.ts | 40 ++ cloud/connector/svc/index.ts | 3 + cloud/connector/svc/package.json | 43 ++ cloud/connector/svc/svc-fpcc-protocol.ts | 462 +++++++++++++ cloud/connector/svc/tsconfig.json | 6 + core/protocols/cloud/msger.ts | 4 +- core/runtime/utils.ts | 11 +- core/tests/blockstore/standalone.test.ts | 4 +- .../fireproof/attachable-subscription.test.ts | 4 +- core/tests/fireproof/attachable.test.ts | 4 +- core/tests/fireproof/fireproof.test.ts | 4 +- core/types/base/types.ts | 1 + pnpm-lock.yaml | 609 +----------------- use-fireproof/fp-cloud-connect-strategy.ts | 8 +- use-fireproof/overlay-html-defaults.tsx | 16 +- use-fireproof/window-open-fp-cloud.ts | 6 +- 24 files changed, 1044 insertions(+), 774 deletions(-) create mode 100644 cloud/connector/svc/clerk-fpcc-evt-entity.ts create mode 100644 cloud/connector/svc/fp-cloud-connector.ts create mode 100644 cloud/connector/svc/index.ts create mode 100644 cloud/connector/svc/package.json create mode 100644 cloud/connector/svc/svc-fpcc-protocol.ts create mode 100644 cloud/connector/svc/tsconfig.json diff --git a/cloud/backend/base/ws-sockets.test.ts b/cloud/backend/base/ws-sockets.test.ts index 7f0d73b30..d820d9636 100644 --- a/cloud/backend/base/ws-sockets.test.ts +++ b/cloud/backend/base/ws-sockets.test.ts @@ -1,13 +1,13 @@ -import { sleep } from "@fireproof/core-runtime"; import * as ps from "@fireproof/core-types-protocols-cloud"; import { Msger } from "@fireproof/core-protocols-cloud"; import { testSuperThis } from "@fireproof/cloud-base"; -import { Future, URI } from "@adviser/cement"; +import { Future, URI, sleep } from "@adviser/cement"; import { describe, beforeAll, afterAll, it, expect, assert } from "vitest"; import { mockJWK, MockJWK } from "./test-helper.js"; const { MsgIsResChat, buildReqChat } = ps; + describe("test multiple connections", () => { const sthis = testSuperThis(); const fpUrl = URI.from(sthis.env.get("FP_ENDPOINT")); diff --git a/cloud/connector/base/fpcc-protocol.ts b/cloud/connector/base/fpcc-protocol.ts index 6aec9177b..42b5adadd 100644 --- a/cloud/connector/base/fpcc-protocol.ts +++ b/cloud/connector/base/fpcc-protocol.ts @@ -20,10 +20,29 @@ import { isFPCCReqWaitConnectorReady, validateFPCCMessage, } from "./protocol-fp-cloud-conn.js"; -import { Logger, OnFunc } from "@adviser/cement"; +import { Logger, OnFunc, Result } from "@adviser/cement"; import { ensureLogger } from "@fireproof/core-runtime"; import { SuperThis } from "@fireproof/core-types-base"; +export interface JustReady { + readonly type: "ready"; +} + +export interface PeerReady { + readonly type: "peer"; + readonly peer: string; +} + +export function isJustReady(obj: unknown): obj is JustReady { + return typeof obj === "object" && obj !== null && (obj as JustReady).type === "ready"; +} + +export function isPeerReady(obj: unknown): obj is PeerReady { + return typeof obj === "object" && obj !== null && (obj as PeerReady).type === "peer" && typeof (obj as PeerReady).peer === "string"; +} + +export type Ready = JustReady | PeerReady; + export interface FPCCProtocol { // handle must be this bound method hash: () => string; @@ -31,7 +50,7 @@ export interface FPCCProtocol { sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void; handleError: (error: unknown) => void; injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void; - ready(): Promise; + ready(): Promise>; stop(): void; } @@ -143,9 +162,10 @@ export class FPCCProtocolBase implements FPCCProtocol { throw new Error("Method not implemented."); }; - ready(): Promise { - - return Promise.resolve(this); + ready(): Promise> { + return Promise.resolve(Result.Ok({ + type: "ready" as const, + })) } injectSend(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void { diff --git a/cloud/connector/base/package.json b/cloud/connector/base/package.json index 79b29e172..c9d51d008 100644 --- a/cloud/connector/base/package.json +++ b/cloud/connector/base/package.json @@ -30,7 +30,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.58", + "@adviser/cement": "^0.4.62", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", "@fireproof/core-types-protocols-cloud": "workspace:*", diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json index 9126f49fd..20e21d525 100644 --- a/cloud/connector/iframe/package.json +++ b/cloud/connector/iframe/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.58", + "@adviser/cement": "^0.4.62", "@clerk/clerk-js": "^5.102.0", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-protocols-dashboard": "workspace:*", diff --git a/cloud/connector/page/package.json b/cloud/connector/page/package.json index 5691f1ddd..caf3319fa 100644 --- a/cloud/connector/page/package.json +++ b/cloud/connector/page/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.58", + "@adviser/cement": "^0.4.62", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", diff --git a/cloud/connector/page/page-fpcc-protocol.ts b/cloud/connector/page/page-fpcc-protocol.ts index 952d3514d..3edbabcb1 100644 --- a/cloud/connector/page/page-fpcc-protocol.ts +++ b/cloud/connector/page/page-fpcc-protocol.ts @@ -1,6 +1,6 @@ -import { ensureLogger, hashObjectSync, sleep } from "@fireproof/core-runtime"; +import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; import { SuperThis } from "@fireproof/core-types-base"; -import { Future, KeyedResolvOnce, Lazy, Logger, ResolveOnce, Result } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Lazy, Logger, poller, ResolveOnce, Result, timeouted } from "@adviser/cement"; import { FPCCProtocol, FPCCProtocolBase, @@ -10,18 +10,48 @@ import { FPCCReqRegisterLocalDbName, FPCCReqWaitConnectorReady, FPCCSendMessage, - isFPCCEvtApp, - isFPCCEvtConnectorReady, - isFPCCEvtNeedsLogin, dbAppKey, + FPCCEvtNeedsLogin, + Ready, + isPeerReady, } from "@fireproof/cloud-connector-base"; +import { initializeIframe } from "./page-handler.js"; +export interface FPCloudFrontend { + hash(): string; + openLogin(msg: FPCCEvtNeedsLogin): void; + stop(): void; +} + +export interface ResultActivePeerTimeout { + readonly state: "timeout"; +} + +export interface ResultActivePeerConnected { + readonly state: "connected"; + readonly peer: string; // url +} + +export interface ResultActivePeerError { + readonly state: "error"; + readonly error: Error; +} + +export type ResultActivePeer = ResultActivePeerTimeout | ResultActivePeerConnected | ResultActivePeerError; + +export interface ConnectorReadyTimeouts { + readonly localMs: number; // 300, + readonly remoteMs: number; //1000, +} export interface PageFPCCProtocolOpts { - readonly maxConnectRetries?: number; readonly iframeHref: string; + readonly fpCloudFrontend: FPCloudFrontend; readonly loginWaitTime?: number; readonly registerWaitTime?: number; readonly intervalMs?: number; + readonly sthis?: SuperThis; + readonly maxConnectRetries?: number; + readonly connectorReadyTimeouts?: Partial; } interface WaitForFPCCEvtApp { @@ -29,48 +59,50 @@ interface WaitForFPCCEvtApp { readonly fpccEvtApp: FPCCEvtApp; } -export class PageFPCCProtocol implements FPCCProtocol { - readonly sthis: SuperThis; +class PageFPCCProtocol implements FPCCProtocol { + // readonly sthis: SuperThis; readonly logger: Logger; readonly fpccProtocol: FPCCProtocolBase; readonly maxConnectRetries: number; - readonly dst: string; + readonly iFrameHref: string; // readonly futureConnected = new Future(); readonly registerFPCCEvtApp = new KeyedResolvOnce(); - readonly waitforFPCCEvtAppFutures = new Map>(); - waitForConnection?: ReturnType; + // readonly waitforFPCCEvtAppFutures = new Map>(); readonly loginWaitTime: number; + readonly registerWaitTime: number; readonly starter = new ResolveOnce(); + readonly sthis: SuperThis; + + readonly connectorReadyTimeouts: ConnectorReadyTimeouts; + readonly fpCloudFrontend: FPCloudFrontend; - readonly hash: () => string; - - constructor(sthis: SuperThis, iopts: PageFPCCProtocolOpts) { - const opts = { - maxConnectRetries: 20, - loginWaitTime: 30000, - ...iopts, - } as Required; - this.sthis = sthis; - this.dst = opts.iframeHref.toString(); - this.logger = ensureLogger(sthis, "PageFPCCProtocol", { - iFrameHref: this.dst, + readonly hash = Lazy((val?: string) => val || ""); // setup in constructor + + constructor(opts: Required & { hash: string }) { + this.sthis = opts.sthis; + this.iFrameHref = opts.iframeHref; + this.logger = ensureLogger(opts.sthis, "PageFPCCProtocol", { + iFrameHref: this.iFrameHref, }); - this.hash = Lazy(() => hashObjectSync(opts)); - this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); + this.fpccProtocol = new FPCCProtocolBase(opts.sthis, this.logger); this.maxConnectRetries = opts.maxConnectRetries; this.loginWaitTime = opts.loginWaitTime; + this.fpCloudFrontend = opts.fpCloudFrontend; + this.registerWaitTime = opts.registerWaitTime; + this.connectorReadyTimeouts = { + localMs: 300, + remoteMs: 1000, + ...opts.connectorReadyTimeouts, + }; + this.hash(opts.hash); } stop(): void { this.fpccProtocol.stop(); - this.waitforFPCCEvtAppFutures.clear(); + // this.waitforFPCCEvtAppFutures.clear(); this.registerFPCCEvtApp.reset(); this.starter.reset(); - if (this.waitForConnection) { - clearInterval(this.waitForConnection); - this.waitForConnection = undefined; - } } getAppId(): string { @@ -83,50 +115,51 @@ export class PageFPCCProtocol implements FPCCProtocol { }; async registerDatabase(dbName: string, ireg: Partial = {}): Promise> { - return this.ready().then(() => { + return this.ready().then((peer) => { + if (!isPeerReady(peer)) { + return Result.Err(new Error("FPCC Protocol not ready - cannot register database")); + } const sreg = { ...ireg, - tid: ireg.tid, type: "FPCCReqRegisterLocalDbName", appId: ireg.appId ?? this.getAppId(), appURL: ireg.appURL ?? window.location.href, dbName, - dst: ireg.dst ?? this.dst, + dst: ireg.dst ?? peer.peer, } satisfies FPCCSendMessage; const key = dbAppKey(sreg); return this.registerFPCCEvtApp .get(key) .once(async () => { - if (this.waitforFPCCEvtAppFutures.has(key)) { - return this.logger - .Error() - .Any({ - key: dbAppKey(sreg), - }) - .Msg("multiple waitforFPCCEvtAppFuture in flight") - .ResultError(); - } + const tid = this.sthis.nextId(12).str; const fpccEvtAppFuture = new Future(); - this.waitforFPCCEvtAppFutures.set(key, fpccEvtAppFuture); - const reg = this.sendMessage(sreg); - - const rFPCCEvtApp = await Promise.race([ + this.fpccProtocol.onFPCCEvtApp((evt, _srcEvent) => { + if (evt.tid === tid) { + fpccEvtAppFuture.resolve(evt); + } + }); + const reg = this.sendMessage( + { + ...sreg, + tid, + src: `page-${window.location.href}-${this.hash()}-${key}`, + dst: peer.peer, + }, + peer.peer, + ); + const rTimeoutFPCCEvtApp = await timeouted( fpccEvtAppFuture .asPromise() .then((evt) => Result.Ok(evt)) .catch((error) => Result.Err(error)), - sleep(this.loginWaitTime).then(() => - this.logger - .Error() - .Any({ - loginWaitTime: this.loginWaitTime, - key: dbAppKey(reg), - }) - .Msg("timeout waiting for FPCCEvtApp") - .ResultError(), - ), - ]); - this.waitforFPCCEvtAppFutures.delete(key); + { + timeout: this.registerWaitTime, + }, + ); + if (rTimeoutFPCCEvtApp.state !== "success") { + throw new Error(`Timeout waiting for FPCCEvtApp for db "${dbName}"`); + } + const rFPCCEvtApp = rTimeoutFPCCEvtApp.value; if (rFPCCEvtApp.isErr()) { throw Result.Err(rFPCCEvtApp); } @@ -148,81 +181,135 @@ export class PageFPCCProtocol implements FPCCProtocol { this.fpccProtocol.injectSend(send); } - sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void { - this.fpccProtocol.sendMessage(event, srcEvent); + sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent | string): T { + return this.fpccProtocol.sendMessage(event, srcEvent); } - readonly ready = Lazy(async (): Promise => { - return this.starter - .once(async () => { - await this.fpccProtocol.ready(); - let maxTries = 0; - const appId = this.getAppId(); - - - - + async tryRegisterApp(dst: string, waitTimeMs: number): Promise { + const tid = this.sthis.nextId(12).str; + let seq = 0; + const isReady = new Future(); + this.fpccProtocol.onFPCCEvtConnectorReady((msg: FPCCMessage) => { + if (msg.tid !== tid) { + return; + } + isReady.resolve(); + }); - this.waitForConnection = setInterval(() => { - if (maxTries > this.maxConnectRetries) { - this.logger.Error().Msg("FPCC iframe connection timeout."); - clearInterval(this.waitForConnection); - this.waitForConnection = undefined; - return; - } - if (maxTries && maxTries % ~~(this.maxConnectRetries / 2) === 0) { - this.logger.Warn().Int("tried", maxTries).Msg("Waiting for FPCC iframe connector to be ready..."); - } - this.fpccProtocol.sendMessage({ - src: window.location.href, + const abortController = new AbortController(); + const polling = poller( + async () => { + this.sendMessage( + { type: "FPCCReqWaitConnectorReady", - dst: "iframe", - seq: maxTries++, + tid, + src: `page-${window.location.href}-${this.hash()}`, + dst, + appId: this.getAppId(), timestamp: Date.now(), - appId, - }); - }, 100); - const waitForConnectorReady = new Future(); - - // this.fpccProtocol.onFPCCEvtNeedsLogin((msg: FPCCMessage): boolean | undefined => { - // this.logger.Info().Any(msg).Msg("Received needs login event from FPCC iframe"); - // this.onFPCCEvtNeedsLoginFns.forEach((cb) => cb(msg)); - // }) - // console.log("PageFPCCProtocol received message", msg); - // switch (true) { - case isFPCCEvtNeedsLogin(msg): { - break; - } - case isFPCCEvtApp(msg): { - const key = dbAppKey({ - appId: msg.appId, - dbName: msg.localDb.dbName, - }); - const future = this.waitforFPCCEvtAppFutures.get(key); - // console.log("PAGE-Received FPCCEvtApp for key", key, msg, future); - if (future) { - future.resolve(msg); - this.waitforFPCCEvtAppFutures.delete(key); + seq: seq++, + }, + dst, + ); + return { + state: "waiting", + abortSignal: abortController.signal, + }; + }, + { + timeoutMs: waitTimeMs, + intervalMs: ~~(waitTimeMs / 5) + 1, + }, + ); + return Promise.race([ + polling.then((res) => { + if (res.state === "error") { + return { + state: "error" as const, + error: res.error, + }; + } + return { + state: "timeout" as const, + }; + }), + isReady.asPromise().then(() => { + abortController.abort(); + return { + state: "connected" as const, + peer: dst, + }; + }), + ]); + } + + readonly ready = Lazy(async (): Promise> => { + return this.starter.once(async () => { + const rReady = await this.fpccProtocol.ready(); + if (rReady.isErr()) { + return Result.Err(rReady); + } + window.addEventListener("message", this.fpccProtocol.handleMessage); + // try my self + const tryRegisterApps: Promise[] = [ + this.tryRegisterApp(window.location.href, this.connectorReadyTimeouts.localMs), + ]; + if (window.parent !== window) { + tryRegisterApps.push( + this.tryRegisterApp(window.parent.location.href, this.connectorReadyTimeouts.remoteMs).then((res) => { + switch (res.state) { + case "connected": + return res; + case "error": + return res; + case "timeout": { + return initializeIframe(this.fpccProtocol, this.iFrameHref).then((iframe) => + this.tryRegisterApp(iframe.src, this.connectorReadyTimeouts.remoteMs), + ); } - this.onFPCCEvtAppFns.forEach((cb) => cb(msg)); - break; } + }), + ); + } else { + const dstUrl = await initializeIframe(this.fpccProtocol, this.iFrameHref); + tryRegisterApps.push(this.tryRegisterApp(dstUrl.src, this.connectorReadyTimeouts.remoteMs)); + } + const result = await Promise.race(tryRegisterApps); + if (result.state === "error") { + return Result.Err(result.error); + } + if (result.state === "timeout") { + return Result.Err("Could not connect to FPCC Svc - timeout"); + } - case isFPCCEvtConnectorReady(msg): { - clearInterval(this.waitForConnection); - this.waitForConnection = undefined; - waitForConnectorReady.resolve(); - return true; - } - } - return undefined; - }); - return waitForConnectorReady.asPromise(); - }) - .then(() => this); - }) - - sendMessage(msg: FPCCSendMessage, srcEvent = new MessageEvent("sendMessage")): T { - return this.fpccProtocol.sendMessage(msg, srcEvent); - } + this.fpccProtocol.onFPCCEvtNeedsLogin((msg) => { + // this could be called multiple times - let the frontend handle it + this.fpCloudFrontend.openLogin(msg); + // we might start polling for connection here + }); + return Result.Ok({ + type: "peer" as const, + peer: result.peer, + }); + }); + }); } + +const keyedPageFPCCProtocols = new KeyedResolvOnce(); +export const pageFPCCProtocol = Lazy((iopts: PageFPCCProtocolOpts) => { + const opts = { + maxConnectRetries: 20, + loginWaitTime: 30000, + ...iopts, + connectorReadyTimeouts: { + localMs: iopts.connectorReadyTimeouts?.localMs ?? 300, + remoteMs: iopts.connectorReadyTimeouts?.remoteMs ?? 1000, + }, + sthis: iopts.sthis ?? ensureSuperThis(), + } as Required; + const hash = hashObjectSync({ + ...opts, // need some more love + fpCloudFrontendHash: opts.fpCloudFrontend.hash(), + }); + return keyedPageFPCCProtocols.get(hash).once(() => new PageFPCCProtocol({ ...opts, hash })); +}); diff --git a/cloud/connector/page/page-handler.ts b/cloud/connector/page/page-handler.ts index 21cb6bad6..8a1f879ad 100644 --- a/cloud/connector/page/page-handler.ts +++ b/cloud/connector/page/page-handler.ts @@ -4,7 +4,6 @@ import { Future } from "@adviser/cement"; import { Writable } from "ts-essentials"; -import { PageFPCCProtocol } from "./page-fpcc-protocol.js"; import { FPCCMessage, FPCCProtocolBase } from "@fireproof/cloud-connector-base"; /** @@ -49,8 +48,7 @@ export function initializeIframe(pageProtocol: FPCCProtocolBase, iframeSrc: stri // Add load event listener // console.log("Initializing FPCC iframe with src:", iframeHref.toString()); iframe.addEventListener("load", () => { - window.addEventListener("message", pageProtocol.handleMessage); - pageProtocol.injectSend((event: Writable) => { + pageProtocol.injectSend((event: Writable) => { // console.log("Sending PageFPCCProtocol", event, iframe.src); event.dst = iframe.src; event.src = window.location.href; diff --git a/cloud/connector/svc/clerk-fpcc-evt-entity.ts b/cloud/connector/svc/clerk-fpcc-evt-entity.ts new file mode 100644 index 000000000..0050e66bc --- /dev/null +++ b/cloud/connector/svc/clerk-fpcc-evt-entity.ts @@ -0,0 +1,208 @@ +import { Lazy, Logger, poller, Result } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core-types-base"; +import { DashApi, AuthType, isResCloudDbTokenBound } from "@fireproof/core-protocols-dashboard"; +import { BackendFPCC, GetCloudDbTokenResult } from "../svc/svc-fpcc-protocol.js"; +import { ensureLogger, exceptionWrapper, } from "@fireproof/core-runtime"; +import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; +import { Clerk } from "@clerk/clerk-js/headless"; +import { DbKey, FPCCEvtApp, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; + +export const clerkSvc = Lazy(async (dashApi: DashApi) => { + const clerkPubKey = await dashApi.getClerkPublishableKey({}); + // console.log("clerkSvc got publishable key", rClerkPubKey); + const clerk = new Clerk(clerkPubKey.publishableKey); + await clerk.load(); + clerk.addListener((session) => { + if (session.user) { + dashApi.logger.Info().Any({ + user: session.user, + windowLocation: window.location.href, + clerkPubKey, + }).Msg("Svc Clerk-User signed in") + } else { + dashApi.logger.Info().Any({ + windowLocation: window.location.href, + clerkPubKey, + }).Msg("Svc Clerk-User signed out") + } + }); + + return clerk; +}); + +// const clerkFPCCEvtEntities = new KeyedResolvOnce(); + +export class ClerkFPCCEvtEntity implements BackendFPCC { + readonly appId: string; + readonly dbName: string; + readonly deviceId: string; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly dashApi: DashApi; + state: "needs-login" | "waiting" | "ready" = "needs-login"; + constructor(sthis: SuperThis, dashApi: DashApi, dbKey: DbKey, deviceId: string) { + this.logger = ensureLogger(sthis, `ClerkFPCCEvtEntity`, { + appId: dbKey.appId, + dbName: dbKey.dbName, + deviceId: deviceId, + }); + this.appId = dbKey.appId; + this.dbName = dbKey.dbName; + this.deviceId = deviceId; + this.sthis = sthis; + this.dashApi = dashApi; + } + + async getCloudDbToken(auth: AuthType): Promise> { + const rRes = await this.dashApi.getCloudDbToken({ + auth, + appId: this.appId, + localDbName: this.dbName, + deviceId: this.deviceId, + }); + if (rRes.isErr()) { + return Result.Err(rRes); + } + const res = rRes.Ok(); + if (!isResCloudDbTokenBound(res)) { + return Result.Ok({ res }); + } + const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); + if (rTandC.isErr()) { + return Result.Err(rTandC); + } + return Result.Ok({ res, claims: rTandC.Ok().claims }); + } + + isUserLoggedIn(): Promise { + return clerkSvc(this.dashApi).then((clerk) => { + return !!clerk.user; + }); + } + + async getDashApiToken(): Promise> { + return exceptionWrapper(async () => { + const clerk = await clerkSvc(this.dashApi); + if (!clerk.user) { + return Result.Err(new Error("User not logged in")); + } + const token = await clerk.session?.getToken({ template: "with-email" }); + if (!token) { + return Result.Err(new Error("No session token available")); + } + return Result.Ok({ + type: "clerk", + token, + }); + }); + } + + isFPCCEvtAppReady(): boolean { + // need to implement a check which looks into the token if it is expired or not + return this.state === "ready"; + } + + fpccEvtApp?: FPCCEvtApp; + getFPCCEvtApp(): Promise> { + return Promise.resolve(this.fpccEvtApp ? Result.Ok(this.fpccEvtApp) : Result.Err(new Error("No FPCCEvtApp registered"))); + } + + setFPCCEvtApp(app: FPCCEvtApp): Promise { + this.fpccEvtApp = app; + return Promise.resolve(); + } + getState(): "needs-login" | "waiting" | "ready" { + // For testing purposes, we always return "needs-login" + return this.state; + } + + setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready" { + const prev = this.state; + this.state = state; + return prev; + } + + // + async waitForAuthToken(resultId: string): Promise> { + return poller(async () => { + const clerk = await clerkSvc(this.dashApi); + if (!clerk.user) { + return { + state: "waiting", + }; + } + // console.log("clerk user is logged in:", clerk.user); + + const rWaitForToken = await this.dashApi.waitForToken({ resultId }, this.logger); + if (rWaitForToken.isErr()) { + return { + state: "error", + error: rWaitForToken.Err(), + }; + } + const waitedTokenByResultId = rWaitForToken.unwrap(); + if (waitedTokenByResultId.status === "found" && waitedTokenByResultId.token) { + const token = waitedTokenByResultId.token; + if (!token) { + return { + state: "error", + error: new Error("No token received"), + }; + } + const rTokenClaims = await convertToTokenAndClaims(this.dashApi, this.logger, token); + if (rTokenClaims.isErr()) { + return { + state: "error", + error: rTokenClaims.Err(), + }; + } + return { + state: "success", + result: rTokenClaims.Ok(), + }; + } + return { state: "waiting" }; + }).then((res) => { + switch (res.state) { + case "success": + return Result.Ok(res.result); + case "error": + return Result.Err(res.error); + default: + return Result.Err("should not happen"); + } + }); + } + + // async getTokenForDb( + // dbInfo: DbKey, + // authToken: TokenAndSelectedTenantAndLedger, + // originEvt: Partial, + // ): Promise { + // await sleep(50); + // return { + // ...dbInfo, + // tid: originEvt.tid ?? this.sthis.nextId(12).str, + // type: "FPCCEvtApp", + // src: "fp-cloud-connector", + // dst: originEvt.src ?? "iframe", + // appFavIcon: { + // defURL: "https://example.com/favicon.ico", + // }, + // devId: this.deviceId, + // user: { + // name: "Test User", + // email: "test@example.com", + // provider: "google", + // iconURL: "https://example.com/icon.png", + // }, + // localDb: { + // dbName: dbInfo.dbName, + // tenantId: "tenant-for-" + dbInfo.appId, + // ledgerId: "ledger-for-" + dbInfo.appId, + // accessToken: `auth-token-for-${dbInfo.appId}-${dbInfo.dbName}-with-${authToken}`, + // }, + // env: {}, + // }; + // } +} diff --git a/cloud/connector/svc/fp-cloud-connector.ts b/cloud/connector/svc/fp-cloud-connector.ts new file mode 100644 index 000000000..a826610a1 --- /dev/null +++ b/cloud/connector/svc/fp-cloud-connector.ts @@ -0,0 +1,40 @@ +import { ensureSuperThis } from "@fireproof/core-runtime"; +import { BuildURI, Lazy, URI } from "@adviser/cement"; +import { FPCCMessage } from "@fireproof/cloud-connector-base"; +import { SvcFPCCProtocol, SvcFPCCProtocolOpts } from "./svc-fpcc-protocol.js"; + +export function defaultFPCloudConnectorOpts( + opts: Partial< + SvcFPCCProtocolOpts & { + readonly loadUrlStr: string; + } + >, +): SvcFPCCProtocolOpts { + const loadUrl = URI.from(opts.loadUrlStr ?? window.location.href); + const dashboardURI = opts.dashboardURI ?? loadUrl.getParam("dashboard_uri"); + let cloudApiURI = opts.cloudApiURI ?? loadUrl.getParam("cloud_api_uri"); + if (dashboardURI && !cloudApiURI) { + cloudApiURI = BuildURI.from(dashboardURI).pathname("/api").toString(); + } + return { + dashboardURI: dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud", + cloudApiURI: cloudApiURI ?? "https://dev.connect.fireproof.direct/api", + }; +} + +export const fpCloudConnector = Lazy((opts: SvcFPCCProtocolOpts) => { + const sthis = opts.sthis ?? ensureSuperThis(); + const protocol = new SvcFPCCProtocol(sthis, opts); + window.addEventListener("message", protocol.handleMessage); + protocol.injectSend((event: FPCCMessage, srcEvent: MessageEvent | string) => { + (event as { src: string }).src = event.src ?? window.location.href; + if (typeof srcEvent === "string") { + window.postMessage(event, srcEvent); + return event; + } else { + srcEvent.source?.postMessage(event, { targetOrigin: srcEvent.origin }); + } + return event; + }); + return protocol; +}); diff --git a/cloud/connector/svc/index.ts b/cloud/connector/svc/index.ts new file mode 100644 index 000000000..89b695163 --- /dev/null +++ b/cloud/connector/svc/index.ts @@ -0,0 +1,3 @@ +export * from "./clerk-fpcc-evt-entity.js"; +export * from "./fp-cloud-connector.js"; +export * from "./svc-fpcc-protocol.js"; \ No newline at end of file diff --git a/cloud/connector/svc/package.json b/cloud/connector/svc/package.json new file mode 100644 index 000000000..f2c68a76b --- /dev/null +++ b/cloud/connector/svc/package.json @@ -0,0 +1,43 @@ +{ + "name": "@fireproof/cloud-connector-svc", + "version": "0.0.0", + "description": "cloud connector shared", + "type": "module", + "main": "./index.js", + "scripts": { + "build": "core-cli tsc", + "pack": "core-cli build --doPack", + "publish": "core-cli build" + }, + "keywords": [ + "ledger", + "JSON", + "document", + "IPLD", + "CID", + "IPFS" + ], + "contributors": [ + "Meno Abels" + ], + "author": "Meno Abels", + "license": "AFL-2.0", + "homepage": "https://use-fireproof.com", + "repository": { + "type": "git", + "url": "git+https://github.com/fireproof-storage/fireproof.git" + }, + "bugs": { + "url": "https://github.com/fireproof-storage/fireproof/issues" + }, + "dependencies": { + "@adviser/cement": "^0.4.62", + "@clerk/clerk-js": "^5.102.1", + "@fireproof/cloud-connector-base": "workspace:*", + "@fireproof/core-protocols-dashboard": "workspace:*", + "@fireproof/core-runtime": "workspace:*", + "@fireproof/core-types-base": "workspace:*", + "@fireproof/core-types-protocols-cloud": "workspace:*", + "ts-essentials": "^10.1.1" + } +} diff --git a/cloud/connector/svc/svc-fpcc-protocol.ts b/cloud/connector/svc/svc-fpcc-protocol.ts new file mode 100644 index 000000000..39cb2bbbf --- /dev/null +++ b/cloud/connector/svc/svc-fpcc-protocol.ts @@ -0,0 +1,462 @@ +import { ensureLogger, hashObjectSync } from "@fireproof/core-runtime"; +import { + convertToTokenAndClaims, + FPCCEvtApp, + FPCCEvtConnectorReady, + FPCCEvtNeedsLogin, + FPCCMessage, + FPCCMsgBase, + FPCCReqRegisterLocalDbName, + FPCCSendMessage, + isFPCCReqRegisterLocalDbName, + FPCCProtocol, + FPCCProtocolBase, + dbAppKey, + DbKey, +} from "@fireproof/cloud-connector-base"; +import { SuperThis } from "@fireproof/core-types-base"; +import { BuildURI, KeyedResolvSeq, Lazy, Logger, Result, sleep } from "@adviser/cement"; +import { + DashApi, + AuthType, + ResCloudDbTokenBound, + ResCloudDbTokenNotBound, + isResCloudDbTokenBound, +} from "@fireproof/core-protocols-dashboard"; +import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; +import { ClerkFPCCEvtEntity, clerkSvc } from "./clerk-fpcc-evt-entity.js"; + +export interface SvcFPCCProtocolOpts { + readonly dashboardURI: string; + readonly cloudApiURI: string; + readonly sthis?: SuperThis; + // readonly backend: BackendFPCC; +} + +export type GetCloudDbTokenResult = + | { + readonly res: ResCloudDbTokenBound; + readonly claims: TokenAndSelectedTenantAndLedger["claims"]; + } + | { + readonly res: ResCloudDbTokenNotBound; + }; +export interface BackendFPCC { + readonly appId: string; + readonly dbName: string; + readonly deviceId: string; + isFPCCEvtAppReady(): boolean; + getState(): "needs-login" | "waiting" | "ready"; + setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready"; + waitForAuthToken(resultId: string): Promise>; + getFPCCEvtApp(): Promise>; + setFPCCEvtApp(app: FPCCEvtApp): Promise; + isUserLoggedIn(): Promise; + getDashApiToken(): Promise>; + // listRegisteredDbNames(): Promise; + getCloudDbToken(auth: AuthType): Promise>; +} + +// function getBackendFromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): BackendFPCC { +// return ClerkFPCCEvtEntity.fromRegisterLocalDbName(sthis, dashApi, req, deviceId); +// } + +const registeredDbs = new Map(); + +// static fromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): ClerkFPCCEvtEntity { +// const key = dbAppKey(req); +// return clerkFPCCEvtEntities.get(key).once(() => new ClerkFPCCEvtEntity(sthis, dashApi, req, deviceId)); +// } + + +export class SvcFPCCProtocol implements FPCCProtocol { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly fpccProtocol: FPCCProtocolBase; + readonly dashboardURI: string; + readonly dashApiURI: string; + readonly dashApi: DashApi; + readonly hash: () => string; + + constructor(sthis: SuperThis, opts: SvcFPCCProtocolOpts) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "SvcFPCCProtocol"); + this.fpccProtocol = new FPCCProtocolBase(sthis, this.logger); + this.hash = Lazy(() => hashObjectSync(opts)); + this.dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/fp/cloud"; + this.dashApiURI = opts.cloudApiURI ?? "https://dev.connect.fireproof.direct/api"; + // console.log("IframeFPCCProtocol constructed with", opts); + this.dashApi = new DashApi(this.sthis, this.dashApiURI); + } + + // readonly activeIframes = new KeyedResolvOnce>(); + // serveIframe(iframe: HTMLIFrameElement): Promise> { + // return this.ready().then(() => { + // return this.activeIframes.get(iframe.src).once(async () => { + // let seq = 0; + + // const tid = this.sthis.nextId(16).str; + // const gotReady = new Future(); + + // this.fpccProtocol.onFPCCEvtConnectorReady((msg) => { + // if (msg.tid !== tid) { + // return; + // } + // gotReady.resolve(); + // this.logger.Info().Any(msg).Msg("Svc-Received connector ready event from iframe"); + // }); + // const abc = new AbortController(); + // const result = await Promise.race([ + // poller( + // () => { + // this.fpccProtocol.sendMessage( + // { + // tid, + // type: "FPCCEvtConnectorReady", + // timestamp: Date.now(), + // seq: seq++, + // devId: "svc-device-id", + // dst: `iframe:${iframe.src}`, + // }, + // iframe.src, + // ); + // return Promise.resolve({ state: "waiting" }); + // }, + // { + // intervalMs: 100, + // exponentialBackoff: true, + // timeoutMs: 10000, + // abortSignal: abc.signal, + // }, + // ).then(() => Promise.resolve(Result.Err("Timeout waiting for iframe to be ready"))), + // gotReady.asPromise().then(() => Result.Ok(abc.abort())), + // ]); + // if (result.isErr()) { + // return this.logger.Warn().Err(result).Msg("Failed to serve iframe").ResultError(); + // } + // return Result.Ok(iframe); + // }); + // }); + // } + + registeredDb(key: DbKey) { + const mapKey = dbAppKey(key); + const existing = registeredDbs.get(mapKey); + if (existing) { + return existing; + } + const newEntity = new ClerkFPCCEvtEntity(this.sthis, this.dashApi, key, this.getDeviceId()); + registeredDbs.set(mapKey, newEntity); + return newEntity; + } + + getDeviceId(): string { + return "we-need-to-implement-device-id"; + } + + async requestPageToDoLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { + const loginTID = this.sthis.nextId(16).str; + const url = BuildURI.from(this.dashboardURI) + .setParam("back_url", "wait-for-token") // dummy back_url since we don't return to the app here + .setParam("result_id", loginTID) + .setParam("app_id", event.appId) + .setParam("local_ledger_name", event.dbName); + if (event.ledger) { + url.setParam("ledger", event.ledger); + } + if (event.tenant) { + url.setParam("tenant", event.tenant); + } + const fpccEvtNeedsLogin: FPCCSendMessage = { + tid: event.tid, + type: "FPCCEvtNeedsLogin", + dst: event.src, + devId: this.getDeviceId(), + loginURL: url.toString(), + loginTID, + loadDbNames: [event], + reason: "BindCloud", + }; + + this.sendMessage(fpccEvtNeedsLogin, srcEvent); + backend.waitForAuthToken(loginTID).then((rAuthToken) => { + if (rAuthToken.isErr()) { + this.logger.Error().Err(rAuthToken).Msg("Failed to obtain auth token after login"); + return; + } + return backend + .getCloudDbToken({ + type: "clerk", + token: rAuthToken.Ok().token, + }) + .then((rCloudToken) => { + if (rCloudToken.isErr()) { + throw this.logger + .Error() + .Err(rCloudToken) + .Any({ + appId: backend.appId, + dbName: backend.dbName, + }) + .Msg("Failed to obtain DB token after login") + .AsError(); + } + const cloudToken = rCloudToken.Ok(); + switch (cloudToken.res.status) { + case "not-bound": + throw this.logger + .Error() + .Str("status", cloudToken.res.status) + .Any({ + appId: backend.appId, + dbName: backend.dbName, + }) + .Msg("DB is still not bound after login") + .AsError(); + } + return cloudToken.res.token; + }) + .then((cloudToken) => convertToTokenAndClaims(this.dashApi, this.logger, cloudToken)) + .then((rTanc) => { + if (rTanc.isErr()) { + throw this.logger + .Error() + .Err(rTanc) + .Any({ + appId: backend.appId, + dbName: backend.dbName, + }) + .Msg("Failed to convert DB token to token and claims after login"); + } + const { claims, token } = rTanc.Ok(); + const fpccEvtApp = { + tid: event.tid, + dst: event.src, + type: "FPCCEvtApp", + appId: backend.appId, + appFavIcon: { + defURL: "https://fireproof.direct/favicon.ico", + }, + devId: "", + user: { + name: claims.nickname ?? claims.userId, + email: claims.email, + provider: claims.provider ?? "unknown", + iconURL: "https://fireproof.direct/favicon.ico", + }, + localDb: { + dbName: backend.dbName, + tenantId: claims.selected.tenant, + ledgerId: claims.selected.ledger, + accessToken: token, + }, + env: {}, // future env vars + } satisfies FPCCSendMessage; + backend.setState("ready"); + backend.setFPCCEvtApp(this.sendMessage(fpccEvtApp, srcEvent)); + // this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); + }); + }); + } + + readonly stateSeq = new KeyedResolvSeq(); + runStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { + return this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + } + + listRegisteredDbs(): BackendFPCC[] { + return Array.from(registeredDbs.values()); + } + + async atomicRunStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { + const bstate = backend.getState(); + switch (true) { + case bstate === "ready" && isFPCCReqRegisterLocalDbName(event): + this.logger.Debug().Msg("Backend is ready, sending FPCCEvtApp"); + return backend + .getFPCCEvtApp() + .then((rFpccEvtApp) => { + if (rFpccEvtApp.isOk()) { + this.sendMessage(rFpccEvtApp.Ok(), srcEvent); + } else { + this.logger.Error().Err(rFpccEvtApp).Msg("Failed to get FPCCEvtApp in ready state"); + } + }) + .then(() => Promise.resolve()); + case bstate === "waiting": + { + this.logger.Debug().Msg("Backend is waiting"); + // this.logger.Info().Str("appID", event.appID).Msg("Backend is waiting"); + throw new Error("Backend is in waiting state; not implemented yet."); + } + break; + case bstate === "needs-login" && isFPCCReqRegisterLocalDbName(event): + { + this.logger.Debug().Msg("Backend needs login", backend.appId, backend.dbName); + const rAuthToken = await backend.getDashApiToken(); + if (rAuthToken.isErr()) { + this.logger + .Warn() + .Err(rAuthToken) + .Any({ appId: backend.appId, dbName: backend.dbName }) + .Msg("User not logged in, requesting login"); + // make all dbs go to waiting state + backend.setState("waiting"); + return this.requestPageToDoLogin(backend, event, srcEvent); + } else { + // const backend = this.registeredDb(event); + + if (backend.isFPCCEvtAppReady()) { + const rFpccEvtApp = await backend.getFPCCEvtApp(); + if (rFpccEvtApp.isErr()) { + this.logger + .Warn() + .Err(rFpccEvtApp) + .Any({ appId: backend.appId, dbName: backend.dbName }) + .Msg("Backend reports error"); + } + if (rFpccEvtApp.isOk()) { + this.logger + .Debug() + .Any({ appId: backend.appId, dbName: backend.dbName, fpccEvtApp: rFpccEvtApp.Ok() }) + .Msg("Sending existing FPCCEvtApp"); + this.sendMessage(rFpccEvtApp.Ok(), srcEvent); + return; + } + } else { + const rDbToken = await this.dashApi.getCloudDbToken({ + auth: rAuthToken.Ok(), + appId: backend.appId, + localDbName: backend.dbName, + deviceId: backend.deviceId, + }); + if (rDbToken.isErr()) { + this.logger + .Error() + .Err(rDbToken) + .Any({ appId: backend.appId, dbName: backend.dbName }) + .Msg("Unexpected error obtaining DB token"); + backend.setState("waiting"); + await sleep(60000); + this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + return; + } + if (rDbToken.Ok().status === "not-bound") { + this.logger.Debug().Any({ appId: backend.appId, dbName: backend.dbName }).Msg("DB is not bound, requesting login"); + // make all dbs go to waiting state + backend.setState("waiting"); + return this.requestPageToDoLogin(backend, event, srcEvent); + } else { + const rCloudToken = await backend.getCloudDbToken(rAuthToken.Ok()); + if (rCloudToken.isErr()) { + this.logger.Warn().Err(rCloudToken).Msg("Failed to obtain DB token, re-running state machine after delay"); + await sleep(1000); + this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + return; + } + const res = rCloudToken.Ok().res; + if (!isResCloudDbTokenBound(res)) { + return this.requestPageToDoLogin(backend, event, srcEvent); + } + const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); + if (rTandC.isErr()) { + this.logger + .Warn() + .Err(rTandC) + .Msg("Failed to convert DB token to token and claims, re-running state machine after delay"); + await sleep(1000); + this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + return; + } + const { token, claims } = rTandC.Ok(); + const fpccEvtApp: FPCCEvtApp = { + tid: event.tid, + type: "FPCCEvtApp", + src: "iframe", + dst: event.src, + appId: backend.appId, + appFavIcon: { + defURL: "https://fireproof.direct/favicon.ico", + }, + devId: backend.deviceId, + user: { + name: claims.nickname ?? claims.userId, + email: claims.email, + provider: claims.provider ?? "unknown", + iconURL: "https://fireproof.direct/favicon.ico", + }, + localDb: { + dbName: backend.dbName, + tenantId: claims.selected.tenant, + ledgerId: claims.selected.ledger, + accessToken: token, + }, + env: {}, + }; + await backend.setFPCCEvtApp(fpccEvtApp); + backend.setState("ready"); + this.logger + .Debug() + .Any({ appId: backend.appId, dbName: backend.dbName }) + .Msg("Sent FPCCEvtApp after obtaining DB token"); + this.sendMessage(fpccEvtApp, srcEvent); + return; + } + } + } + } + break; + + default: + throw this.logger.Error().Str("state", bstate).Msg("Unknown backend state").AsError(); + } + } + + readonly handleError = (_error: unknown): void => { + throw new Error("Method not implemented."); + }; + + readonly handleMessage = (event: MessageEvent): void => { + this.fpccProtocol.handleMessage(event); + } + + stop(): void { + this.logger.Debug().Msg("SvcFPCCProtocol stop called"); + this.fpccProtocol.stop(); + } + + injectSend(sendFn: (evt: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void { + this.fpccProtocol.injectSend(sendFn); + } + + readonly ready = Lazy(async (): Promise => { + await clerkSvc(this.dashApi); + await this.fpccProtocol.ready(); + + this.fpccProtocol.onFPCCReqWaitConnectorReady(async (event, srcEvent: MessageEvent) => { + this.logger.Info().Str("appID", event.appId).Msg("Received request to wait for connector ready"); + // Here you would implement logic to handle the wait for connector ready request + const readyEvent: FPCCSendMessage = { + tid: event.tid, + type: "FPCCEvtConnectorReady", + timestamp: Date.now(), + seq: event.seq, + devId: this.getDeviceId(), + dst: event.src, + }; + this.sendMessage(readyEvent, srcEvent); + }) + + this.fpccProtocol.onFPCCMessage(async (event, srcEvent: MessageEvent) => { + return this.runStateMachine(backend, event, srcEvent); + }); + + return this; + }); + + sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent | string): T { + // message.src = window.location.href; + // console.log("IframeFPCCProtocol sendMessage called", message); + return this.fpccProtocol.sendMessage(message, srcEvent); + } +} diff --git a/cloud/connector/svc/tsconfig.json b/cloud/connector/svc/tsconfig.json new file mode 100644 index 000000000..a7db7c68a --- /dev/null +++ b/cloud/connector/svc/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/core/protocols/cloud/msger.ts b/core/protocols/cloud/msger.ts index e7bd6e894..4cf6bccff 100644 --- a/core/protocols/cloud/msger.ts +++ b/core/protocols/cloud/msger.ts @@ -1,4 +1,4 @@ -import { BuildURI, CoerceURI, Logger, Result, runtimeFn, URI } from "@adviser/cement"; +import { BuildURI, CoerceURI, Logger, Result, runtimeFn, URI, sleep } from "@adviser/cement"; import { buildReqGestalt, defaultGestalt, @@ -30,7 +30,7 @@ import { import { ensurePath, HttpConnection } from "./http-connection.js"; import { WSConnection } from "./ws-connection.js"; import { SuperThis } from "@fireproof/core-types-base"; -import { ensureLogger, sleep } from "@fireproof/core-runtime"; +import { ensureLogger } from "@fireproof/core-runtime"; import pLimit from "@fireproof/vendor/p-limit"; // const headers = { diff --git a/core/runtime/utils.ts b/core/runtime/utils.ts index 16595d7eb..e2ca56e81 100644 --- a/core/runtime/utils.ts +++ b/core/runtime/utils.ts @@ -71,6 +71,11 @@ class SuperThisImpl implements SuperThis { // console.log("superThis", this); } + hash(): string { + return "superthis-hash-is-not-implemented-but-a-dummy"; + + } + nextId(bytes = 6): { str: string; bin: Uint8Array } { const bin = this.crypto.randomBytes(bytes); return { @@ -630,9 +635,9 @@ export async function hashObjectCID, S>(o: T): Promise< return { cid: CID.create(1, json.code, hash), bytes, obj: o }; } -export function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} +// export function sleep(ms: number) { +// return new Promise((resolve) => setTimeout(resolve, ms)); +// } /** * Deep clone a value diff --git a/core/tests/blockstore/standalone.test.ts b/core/tests/blockstore/standalone.test.ts index e539478f1..8462165d4 100644 --- a/core/tests/blockstore/standalone.test.ts +++ b/core/tests/blockstore/standalone.test.ts @@ -1,8 +1,8 @@ -import { BuildURI, runtimeFn, URI } from "@adviser/cement"; +import { BuildURI, runtimeFn, URI, sleep } from "@adviser/cement"; import { Link } from "multiformats"; import { stripper } from "@adviser/cement/utils"; import pLimit from "@fireproof/vendor/p-limit"; -import { ensureSuperThis, sleep } from "@fireproof/core-runtime"; +import { ensureSuperThis, } from "@fireproof/core-runtime"; import { CRDT, PARAM, LedgerOpts } from "@fireproof/core-types-base"; import { describe, it, vi, expect, beforeEach, afterEach } from "vitest"; import { Loader } from "@fireproof/core-blockstore"; diff --git a/core/tests/fireproof/attachable-subscription.test.ts b/core/tests/fireproof/attachable-subscription.test.ts index 2260759f9..3524a4131 100644 --- a/core/tests/fireproof/attachable-subscription.test.ts +++ b/core/tests/fireproof/attachable-subscription.test.ts @@ -1,7 +1,7 @@ -import { AppContext, BuildURI, WithoutPromise } from "@adviser/cement"; +import { AppContext, BuildURI, WithoutPromise, sleep } from "@adviser/cement"; import { Attachable, Database, fireproof, GatewayUrlsParam, PARAM, DocBase } from "@fireproof/core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { ensureSuperThis, sleep } from "@fireproof/core-runtime"; +import { ensureSuperThis } from "@fireproof/core-runtime"; const ROWS = 2; diff --git a/core/tests/fireproof/attachable.test.ts b/core/tests/fireproof/attachable.test.ts index 4f1aff9e1..b5c9c9d87 100644 --- a/core/tests/fireproof/attachable.test.ts +++ b/core/tests/fireproof/attachable.test.ts @@ -1,11 +1,11 @@ -import { AppContext, BuildURI, URI, WithoutPromise } from "@adviser/cement"; +import { AppContext, BuildURI, URI, WithoutPromise, sleep } from "@adviser/cement"; import { stripper } from "@adviser/cement/utils"; import { Attachable, Database, fireproof, GatewayUrlsParam, PARAM, Attached, TraceFn } from "@fireproof/core"; import { CarReader } from "@ipld/car/reader"; import * as dagCbor from "@ipld/dag-cbor"; import { mockLoader } from "../helpers.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ensureSuperThis, sleep } from "@fireproof/core-runtime"; +import { ensureSuperThis, } from "@fireproof/core-runtime"; import { DefSerdeGateway } from "@fireproof/core-gateways-base"; import { MemoryGateway } from "@fireproof/core-gateways-memory"; import { AttachedRemotesImpl } from "@fireproof/core-blockstore"; diff --git a/core/tests/fireproof/fireproof.test.ts b/core/tests/fireproof/fireproof.test.ts index 463d5c9a9..a92ad0227 100644 --- a/core/tests/fireproof/fireproof.test.ts +++ b/core/tests/fireproof/fireproof.test.ts @@ -3,8 +3,8 @@ import { docs } from "./fireproof.test.fixture.js"; import { CID } from "multiformats/cid"; import { Index, index, fireproof, isDatabase } from "@fireproof/core-base"; -import { URI } from "@adviser/cement"; -import { ensureSuperThis, sleep } from "@fireproof/core-runtime"; +import { URI, sleep } from "@adviser/cement"; +import { ensureSuperThis, } from "@fireproof/core-runtime"; import { DocResponse, DocWithId, IndexRows, Database, PARAM, MapFn, ConfigOpts } from "@fireproof/core-types-base"; import { describe, afterEach, beforeEach, it, expect, beforeAll, assert } from "vitest"; import { AnyLink } from "@fireproof/core-types-blockstore"; diff --git a/core/types/base/types.ts b/core/types/base/types.ts index 9c8afe9ea..b6bf5ddbe 100644 --- a/core/types/base/types.ts +++ b/core/types/base/types.ts @@ -159,6 +159,7 @@ export interface SuperThis { nextId(bytes?: number): { str: string; bin: Uint8Array; toString: () => string }; start(): Promise; clone(override: Partial): SuperThis; + hash(): string; } export interface IdleEventFromCommitQueue { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfd838b61..b73408799 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,24 +77,8 @@ importers: cli: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../core/runtime @@ -151,36 +135,14 @@ importers: cloud/3rd-party: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) ->>>>>>> fade9b61 (wip [skip ci]) preact: specifier: ^10.27.2 version: 10.27.2 preact-render-to-string: specifier: ^6.6.2 version: 6.6.2(preact@10.27.2) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) - preact: - specifier: ^10.27.2 - version: 10.27.2 - preact-render-to-string: - specifier: ^6.6.2 - version: 6.6.2(preact@10.27.2) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) react: specifier: ^19.2.0 version: 19.2.0 @@ -207,24 +169,8 @@ importers: cloud/backend/base: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@cloudflare/workers-types': specifier: ^4.20251111.0 version: 4.20251111.0 @@ -287,24 +233,8 @@ importers: cloud/backend/cf-d1: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@cloudflare/workers-types': specifier: ^4.20251111.0 version: 4.20251111.0 @@ -364,24 +294,8 @@ importers: cloud/backend/node: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/cloud-backend-base': specifier: workspace:0.0.0 version: link:../base @@ -441,24 +355,8 @@ importers: cloud/base: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-blockstore': specifier: workspace:0.0.0 version: link:../../core/blockstore @@ -494,7 +392,6 @@ importers: specifier: ^8.8.5 version: 8.8.5 -<<<<<<< HEAD cloud/box-party: dependencies: '@adviser/cement': @@ -523,59 +420,11 @@ importers: specifier: ^7.1.12 version: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) -||||||| parent of e125fa77 (wip [skip ci]) -======= - cloud/box-party: - dependencies: - '@adviser/cement': - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) - '@fireproof/cloud-connector-svc': - specifier: workspace:* - version: link:../connector/svc - react: - specifier: ^19.2.0 - version: 19.2.0 - react-dom: - specifier: ^19.2.0 - version: 19.2.0(react@19.2.0) - use-fireproof: - specifier: workspace:0.0.0 - version: link:../../use-fireproof - devDependencies: - '@types/react': - specifier: ^19.2.2 - version: 19.2.2 - '@types/react-dom': - specifier: ^19.2.2 - version: 19.2.2(@types/react@19.2.2) - '@vitejs/plugin-react': - specifier: ^5.1.0 - version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) - vite: - specifier: ^7.1.12 - version: 7.1.12(@types/node@24.9.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - ->>>>>>> e125fa77 (wip [skip ci]) cloud/connector/base: dependencies: '@adviser/cement': -<<<<<<< HEAD - specifier: ^0.4.54 -<<<<<<< HEAD + specifier: ^0.4.62 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - version: 0.4.62(typescript@5.9.3) -======= - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:* version: link:../../../core/runtime @@ -598,22 +447,8 @@ importers: cloud/connector/iframe: dependencies: '@adviser/cement': -<<<<<<< HEAD - specifier: ^0.4.54 -<<<<<<< HEAD + specifier: ^0.4.62 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - version: 0.4.62(typescript@5.9.3) -======= - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@clerk/clerk-js': specifier: ^5.102.0 version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) @@ -636,22 +471,8 @@ importers: cloud/connector/page: dependencies: '@adviser/cement': -<<<<<<< HEAD - specifier: ^0.4.54 -<<<<<<< HEAD + specifier: ^0.4.62 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - version: 0.4.62(typescript@5.9.3) -======= - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../base @@ -668,11 +489,11 @@ importers: cloud/connector/svc: dependencies: '@adviser/cement': - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) + specifier: ^0.4.62 + version: 0.4.63(typescript@5.9.3) '@clerk/clerk-js': specifier: ^5.102.1 - version: 5.102.1(patch_hash=b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e)(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) + version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../base @@ -725,24 +546,8 @@ importers: cloud/todo-app: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/vendor': specifier: workspace:0.0.0 version: link:../../vendor @@ -772,24 +577,8 @@ importers: core/base: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-blockstore': specifier: workspace:0.0.0 version: link:../blockstore @@ -834,24 +623,8 @@ importers: core/blockstore: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../gateways/base @@ -910,24 +683,8 @@ importers: core/core: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-base': specifier: workspace:0.0.0 version: link:../base @@ -947,24 +704,8 @@ importers: core/device-id: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-keybag': specifier: workspace:0.0.0 version: link:../keybag @@ -994,24 +735,8 @@ importers: core/gateways/base: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../../runtime @@ -1034,24 +759,8 @@ importers: core/gateways/cloud: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -1080,24 +789,8 @@ importers: core/gateways/file: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -1133,24 +826,8 @@ importers: core/gateways/file-deno: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../../types/base @@ -1167,24 +844,8 @@ importers: core/gateways/file-node: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../../types/base @@ -1195,24 +856,8 @@ importers: core/gateways/indexeddb: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -1235,24 +880,8 @@ importers: core/gateways/memory: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-base': specifier: workspace:0.0.0 version: link:../base @@ -1282,24 +911,8 @@ importers: core/keybag: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-gateways-file': specifier: workspace:0.0.0 version: link:../gateways/file @@ -1328,24 +941,8 @@ importers: core/protocols/cloud: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../../runtime @@ -1368,24 +965,8 @@ importers: core/protocols/dashboard: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-runtime': specifier: workspace:0.0.0 version: link:../../runtime @@ -1402,24 +983,8 @@ importers: core/runtime: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@adviser/ts-xxhash': specifier: ^1.0.2 version: 1.0.2 @@ -1455,24 +1020,8 @@ importers: core/tests: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core': specifier: workspace:0.0.0 version: link:../core @@ -1567,9 +1116,6 @@ importers: playwright-chromium: specifier: ^1.56.1 version: 1.56.1 - playwright-core: - specifier: ^1.56.1 - version: 1.56.1 vitest: specifier: ^4.0.8 version: 4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) @@ -1580,24 +1126,8 @@ importers: core/types/base: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-blockstore': specifier: workspace:0.0.0 version: link:../blockstore @@ -1623,24 +1153,8 @@ importers: core/types/blockstore: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../base @@ -1664,24 +1178,8 @@ importers: core/types/protocols/cloud: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/core-types-base': specifier: workspace:0.0.0 version: link:../../base @@ -1708,24 +1206,8 @@ importers: core/types/runtime: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/vendor': specifier: workspace:0.0.0 version: link:../../../vendor @@ -1736,24 +1218,8 @@ importers: dashboard: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@clerk/backend': specifier: ^2.21.0 version: 2.21.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -1848,9 +1314,6 @@ importers: '@fireproof/cloud-connector-iframe': specifier: workspace:0.0.0 version: link:../cloud/connector/iframe - '@fireproof/cloud-connector-svc': - specifier: workspace:0.0.0 - version: link:../cloud/connector/svc '@fireproof/core-cli': specifier: workspace:* version: link:../cli @@ -1930,24 +1393,8 @@ importers: use-fireproof: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../cloud/connector/base @@ -2031,24 +1478,8 @@ importers: vendor: dependencies: '@adviser/cement': -<<<<<<< HEAD specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) -||||||| parent of fade9b61 (wip [skip ci]) - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -======= -<<<<<<< HEAD - specifier: ^0.4.58 - version: 0.4.62(typescript@5.9.3) -||||||| parent of e125fa77 (wip [skip ci]) - specifier: ^0.4.54 - version: 0.4.54(typescript@5.9.3) -======= - specifier: ^0.4.58 - version: 0.4.58(typescript@5.9.3) ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) yocto-queue: specifier: ^1.2.2 version: 1.2.2 @@ -2080,24 +1511,8 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} -<<<<<<< HEAD '@adviser/cement@0.4.63': resolution: {integrity: sha512-ctOtXLi959Ay+lO5fES55ejiNV7EHR0v9GCzCWzdbYlNOh4W165dHpplGoxsrZ2hxzKtQGIk8Qu66YOD+76mhg==} -||||||| parent of fade9b61 (wip [skip ci]) - '@adviser/cement@0.4.62': - resolution: {integrity: sha512-EwfhbibpB6eKXYw8h8CWBueZ44mIOlll08poUBkKsABTqEXbv54dflQAWX5lnRA/FNRD2ThgejWcxPKxvEvacg==} -======= -<<<<<<< HEAD - '@adviser/cement@0.4.62': - resolution: {integrity: sha512-EwfhbibpB6eKXYw8h8CWBueZ44mIOlll08poUBkKsABTqEXbv54dflQAWX5lnRA/FNRD2ThgejWcxPKxvEvacg==} -||||||| parent of e125fa77 (wip [skip ci]) - '@adviser/cement@0.4.54': - resolution: {integrity: sha512-LTJQqGgBb6A3pNVLJFIX2/cnkOt9WPakPFDZXQu8Jpza+ZqapqOo4rCS852ZtchjvorKwRwwQIku4OB/bbVcEg==} -======= - '@adviser/cement@0.4.58': - resolution: {integrity: sha512-w0mRru1fZLgdXPhlwfQs2woPQwXqmHwW20KX/gWBX6e+ftMHAEJcAwIzPC5tPGJXb2bCUjf3XHU17HjlD4e1/Q==} ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) engines: {node: '>=20.19.0'} hasBin: true @@ -6893,19 +6308,7 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} -<<<<<<< HEAD '@adviser/cement@0.4.63(typescript@5.9.3)': -||||||| parent of fade9b61 (wip [skip ci]) - '@adviser/cement@0.4.62(typescript@5.9.3)': -======= -<<<<<<< HEAD - '@adviser/cement@0.4.62(typescript@5.9.3)': -||||||| parent of e125fa77 (wip [skip ci]) - '@adviser/cement@0.4.54(typescript@5.9.3)': -======= - '@adviser/cement@0.4.58(typescript@5.9.3)': ->>>>>>> e125fa77 (wip [skip ci]) ->>>>>>> fade9b61 (wip [skip ci]) dependencies: ts-essentials: 10.1.1(typescript@5.9.3) yaml: 2.8.1 diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index b580faa4b..85fbff39a 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,15 +1,15 @@ -import { Future, KeyedResolvOnce, Lazy, Logger, poller, ResolveSeq } from "@adviser/cement"; +import { Future, KeyedResolvOnce, Lazy, Logger, poller, ResolveSeq, sleep } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { ensureSuperThis, hashObjectSync, sleep } from "@fireproof/core-runtime"; +import { ensureSuperThis, hashObjectSync, } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; import { FPCCProtocol, FPCCProtocolBase, isInIframe } from "@fireproof/cloud-connector-base"; import { useEffect, useState } from "react"; import { defaultFPCloudConnectorOpts, fpCloudConnector } from "../cloud/connector/svc/fp-cloud-connector.js"; import { FPCloudConnectStrategyImpl } from "./fp-cloud-connect-strategy-impl.js"; -import { initializeIframe, PageFPCCProtocolOpts } from "@fireproof/cloud-connector-page"; -import { FPCloudFrontend, FPCloudFrontendImpl} from "./window-open-fp-cloud.js"; +import { FPCloudFrontend, initializeIframe, PageFPCCProtocolOpts } from "@fireproof/cloud-connector-page"; +import { FPCloudFrontendImpl} from "./window-open-fp-cloud.js"; export interface FPCloudConnectOpts extends RedirectStrategyOpts { readonly dashboardURI?: string; diff --git a/use-fireproof/overlay-html-defaults.tsx b/use-fireproof/overlay-html-defaults.tsx index cf109aa22..97be0956f 100644 --- a/use-fireproof/overlay-html-defaults.tsx +++ b/use-fireproof/overlay-html-defaults.tsx @@ -2,15 +2,13 @@ import { React, renderToString } from "./jsx-helper.js"; export function defaultOverlayHtml(redirectLink: string) { return renderToString( - <> -
-
×
- Fireproof Dashboard Sign in to Fireproof Dashboard - - Redirect to Fireproof - -
- , +
+
×
+ Fireproof Dashboard Sign in to Fireproof Dashboard + + Redirect to Fireproof + +
, ); } diff --git a/use-fireproof/window-open-fp-cloud.ts b/use-fireproof/window-open-fp-cloud.ts index 3dac09c99..815e6c1c1 100644 --- a/use-fireproof/window-open-fp-cloud.ts +++ b/use-fireproof/window-open-fp-cloud.ts @@ -5,12 +5,8 @@ import { Logger } from "@adviser/cement"; import { defaultOverlayCss, defaultOverlayHtml } from "./overlay-html-defaults.js"; import { SuperThis } from "@fireproof/core-types-base"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; +import { FPCloudFrontend } from "@fireproof/cloud-connector-page"; -export interface FPCloudFrontend { - hash(): string; - openFireproofLogin(msg: FPCCEvtNeedsLogin): void; - stop(): void; -} export interface FPCloudFrontendImplOpts extends RedirectStrategyOpts { readonly sthis: SuperThis; From f03495449695cfcc9b3889c0ae65191ca841c13e Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 12 Nov 2025 16:52:05 +0100 Subject: [PATCH 23/23] wip[skip ci] --- cloud/3rd-party/src/App.tsx | 1 - cloud/3rd-party/src/main.tsx | 2 +- cloud/3rd-party/src/overlayHtml.tsx | 5 +- cloud/backend/base/ws-sockets.test.ts | 1 - cloud/connector/base/fpcc-protocol.ts | 31 +- cloud/connector/base/index.ts | 26 +- cloud/connector/base/package.json | 2 +- cloud/connector/iframe/package.json | 2 +- cloud/connector/page/package.json | 2 +- cloud/connector/page/page-handler.ts | 2 +- cloud/connector/svc/clerk-fpcc-evt-entity.ts | 63 +- cloud/connector/svc/index.ts | 2 +- cloud/connector/svc/package.json | 2 +- cloud/connector/svc/svc-fpcc-protocol.ts | 221 +++-- cloud/connector/test/package.json | 6 +- .../connector/test/page-fpcc-protocol.test.ts | 26 +- core/protocols/dashboard/msg-api.ts | 2 + core/runtime/utils.ts | 1 - core/tests/blockstore/standalone.test.ts | 2 +- core/tests/fireproof/attachable.test.ts | 2 +- core/tests/fireproof/fireproof.test.ts | 2 +- dashboard/package.json | 2 +- dashboard/vite.config.ts | 7 +- eslint.config.mjs | 1 + package.json | 3 +- pnpm-lock.yaml | 764 +++++++----------- .../fp-cloud-connect-strategy-impl.ts | 4 +- use-fireproof/fp-cloud-connect-strategy.ts | 25 +- .../iframe-fp-cloud-connect-strategy.ts | 74 -- use-fireproof/package.json | 2 - use-fireproof/window-open-fp-cloud.ts | 4 +- 31 files changed, 560 insertions(+), 729 deletions(-) delete mode 100644 use-fireproof/iframe-fp-cloud-connect-strategy.ts diff --git a/cloud/3rd-party/src/App.tsx b/cloud/3rd-party/src/App.tsx index 5ae6ec471..2610b034e 100644 --- a/cloud/3rd-party/src/App.tsx +++ b/cloud/3rd-party/src/App.tsx @@ -5,7 +5,6 @@ import "./App.css"; // import { URI } from "@adviser/cement"; function App() { - const { database, attach } = useFireproof("fireproof-5-party", { attach: toCloud({ strategy: FPCloudConnectStrategy({ diff --git a/cloud/3rd-party/src/main.tsx b/cloud/3rd-party/src/main.tsx index a42202c08..dfc003786 100644 --- a/cloud/3rd-party/src/main.tsx +++ b/cloud/3rd-party/src/main.tsx @@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.jsx"; -console.log("i'm in main.tsx") +console.log("i'm in main.tsx"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion createRoot(document.getElementById("root")!).render( diff --git a/cloud/3rd-party/src/overlayHtml.tsx b/cloud/3rd-party/src/overlayHtml.tsx index 2d7a725ab..2c9b83bfc 100644 --- a/cloud/3rd-party/src/overlayHtml.tsx +++ b/cloud/3rd-party/src/overlayHtml.tsx @@ -1,9 +1,6 @@ import { jsx } from "use-fireproof"; -const { - renderToString, - React -} = jsx +const { renderToString, React } = jsx; // function jsxDEV(...args: unknown[]) { // // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/cloud/backend/base/ws-sockets.test.ts b/cloud/backend/base/ws-sockets.test.ts index d820d9636..d2a092677 100644 --- a/cloud/backend/base/ws-sockets.test.ts +++ b/cloud/backend/base/ws-sockets.test.ts @@ -7,7 +7,6 @@ import { mockJWK, MockJWK } from "./test-helper.js"; const { MsgIsResChat, buildReqChat } = ps; - describe("test multiple connections", () => { const sthis = testSuperThis(); const fpUrl = URI.from(sthis.env.get("FP_ENDPOINT")); diff --git a/cloud/connector/base/fpcc-protocol.ts b/cloud/connector/base/fpcc-protocol.ts index 42b5adadd..812b038e7 100644 --- a/cloud/connector/base/fpcc-protocol.ts +++ b/cloud/connector/base/fpcc-protocol.ts @@ -38,7 +38,9 @@ export function isJustReady(obj: unknown): obj is JustReady { } export function isPeerReady(obj: unknown): obj is PeerReady { - return typeof obj === "object" && obj !== null && (obj as PeerReady).type === "peer" && typeof (obj as PeerReady).peer === "string"; + return ( + typeof obj === "object" && obj !== null && (obj as PeerReady).type === "peer" && typeof (obj as PeerReady).peer === "string" + ); } export type Ready = JustReady | PeerReady; @@ -63,26 +65,27 @@ export class FPCCProtocolBase implements FPCCProtocol { readonly onMessage = OnFunc<(event: MessageEvent) => void>(); readonly onFPCCMessage = OnFunc<(msg: FPCCMessage, srcEvent: MessageEvent) => void>(); - readonly onFPCCEvtNeedsLogin = OnFunc<(msg: FPCCEvtNeedsLogin, srcEvent: MessageEvent) => void>() + readonly onFPCCEvtNeedsLogin = OnFunc<(msg: FPCCEvtNeedsLogin, srcEvent: MessageEvent) => void>(); readonly onFPCCError = OnFunc<(msg: FPCCError, srcEvent: MessageEvent) => void>(); - readonly onFPCCReqRegisterLocalDbName = OnFunc<(msg: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent) => void>() - readonly onFPCCEvtApp = OnFunc<(msg: FPCCEvtApp, srcEvent: MessageEvent) => void>() + readonly onFPCCReqRegisterLocalDbName = OnFunc<(msg: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent) => void>(); + readonly onFPCCEvtApp = OnFunc<(msg: FPCCEvtApp, srcEvent: MessageEvent) => void>(); readonly onFPCCPing = OnFunc<(msg: FPCCPing, srcEvent: MessageEvent) => void>(); - readonly onFPCCPong = OnFunc<(msg: FPCCPong, srcEvent: MessageEvent) => void>() + readonly onFPCCPong = OnFunc<(msg: FPCCPong, srcEvent: MessageEvent) => void>(); readonly onFPCCEvtConnectorReady = OnFunc<(msg: FPCCEvtConnectorReady, srcEvent: MessageEvent) => void>(); - readonly onFPCCReqWaitConnectorReady = OnFunc<(msg: FPCCReqWaitConnectorReady, srcEvent: MessageEvent) => void>(); + readonly onFPCCReqWaitConnectorReady = OnFunc<(msg: FPCCReqWaitConnectorReady, srcEvent: MessageEvent) => void>(); constructor(sthis: SuperThis, logger?: Logger) { this.sthis = sthis; this.logger = logger || ensureLogger(sthis, "FPCCProtocolBase"); - this.onMessage(event => { + this.onMessage((event) => { this.handleMessage(event); }); this.onFPCCMessage((msg, srcEvent) => { this.#handleFPCCMessage(msg, srcEvent); - }) + }); this.onFPCCPing((msg, srcEvent) => { - this.sendMessage({ + this.sendMessage( + { src: msg.dst, dst: msg.src, pingTid: msg.tid, @@ -114,7 +117,6 @@ export class FPCCProtocolBase implements FPCCProtocol { #handleFPCCMessage(event: FPCCMessage, srcEvent: MessageEvent) { this.logger.Debug().Any("event", event).Msg("Handling FPCC message"); switch (true) { - case isFPCCEvtNeedsLogin(event): { this.onFPCCEvtNeedsLogin.invoke(event, srcEvent); break; @@ -154,7 +156,6 @@ export class FPCCProtocolBase implements FPCCProtocol { this.onFPCCReqWaitConnectorReady.invoke(event, srcEvent); break; } - } } @@ -163,9 +164,11 @@ export class FPCCProtocolBase implements FPCCProtocol { }; ready(): Promise> { - return Promise.resolve(Result.Ok({ - type: "ready" as const, - })) + return Promise.resolve( + Result.Ok({ + type: "ready" as const, + }), + ); } injectSend(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void { diff --git a/cloud/connector/base/index.ts b/cloud/connector/base/index.ts index 0f5aa00ea..91ee4064a 100644 --- a/cloud/connector/base/index.ts +++ b/cloud/connector/base/index.ts @@ -1,21 +1,27 @@ +import { hashObjectSync } from "@fireproof/core-runtime"; +import { FPCCReqRegisterLocalDbName } from "./protocol-fp-cloud-conn.js"; + export * from "./convert-to-token-and-claims.js"; export * from "./fpcc-protocol.js"; export * from "./post-messager.js"; export * from "./protocol-fp-cloud-conn.js"; -export interface DbKey { - readonly appId: string; - readonly dbName: string; -} +// export interface DbKey { +// readonly appId: string; +// readonly dbName: string; +// } -export function dbAppKey(o: DbKey): string { - return o.appId + ":" + o.dbName; +export function dbAppKey(o: FPCCReqRegisterLocalDbName): string { + return hashObjectSync(o); + //o.appId + ":" + o.dbName; } -export function isInIframe(win: { - readonly self: Window | null; - readonly top: Window | null; -} = window): boolean { +export function isInIframe( + win: { + readonly self: Window | null; + readonly top: Window | null; + } = window, +): boolean { try { return win.self !== win.top; } catch (e) { diff --git a/cloud/connector/base/package.json b/cloud/connector/base/package.json index c9d51d008..03ef72486 100644 --- a/cloud/connector/base/package.json +++ b/cloud/connector/base/package.json @@ -30,7 +30,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.62", + "@adviser/cement": "^0.4.63", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", "@fireproof/core-types-protocols-cloud": "workspace:*", diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json index 20e21d525..1262fb116 100644 --- a/cloud/connector/iframe/package.json +++ b/cloud/connector/iframe/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.62", + "@adviser/cement": "^0.4.63", "@clerk/clerk-js": "^5.102.0", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-protocols-dashboard": "workspace:*", diff --git a/cloud/connector/page/package.json b/cloud/connector/page/package.json index caf3319fa..f84d0a92d 100644 --- a/cloud/connector/page/package.json +++ b/cloud/connector/page/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.62", + "@adviser/cement": "^0.4.63", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-runtime": "workspace:*", "@fireproof/core-types-base": "workspace:*", diff --git a/cloud/connector/page/page-handler.ts b/cloud/connector/page/page-handler.ts index 8a1f879ad..3c76c38af 100644 --- a/cloud/connector/page/page-handler.ts +++ b/cloud/connector/page/page-handler.ts @@ -48,7 +48,7 @@ export function initializeIframe(pageProtocol: FPCCProtocolBase, iframeSrc: stri // Add load event listener // console.log("Initializing FPCC iframe with src:", iframeHref.toString()); iframe.addEventListener("load", () => { - pageProtocol.injectSend((event: Writable) => { + pageProtocol.injectSend((event: Writable) => { // console.log("Sending PageFPCCProtocol", event, iframe.src); event.dst = iframe.src; event.src = window.location.href; diff --git a/cloud/connector/svc/clerk-fpcc-evt-entity.ts b/cloud/connector/svc/clerk-fpcc-evt-entity.ts index 0050e66bc..5ee34b1ae 100644 --- a/cloud/connector/svc/clerk-fpcc-evt-entity.ts +++ b/cloud/connector/svc/clerk-fpcc-evt-entity.ts @@ -1,11 +1,11 @@ import { Lazy, Logger, poller, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { DashApi, AuthType, isResCloudDbTokenBound } from "@fireproof/core-protocols-dashboard"; -import { BackendFPCC, GetCloudDbTokenResult } from "../svc/svc-fpcc-protocol.js"; -import { ensureLogger, exceptionWrapper, } from "@fireproof/core-runtime"; +import { BackendFPCC, BackendState, GetCloudDbTokenResult } from "../svc/svc-fpcc-protocol.js"; +import { ensureLogger, exceptionWrapper } from "@fireproof/core-runtime"; import { TokenAndSelectedTenantAndLedger } from "@fireproof/core-types-protocols-cloud"; import { Clerk } from "@clerk/clerk-js/headless"; -import { DbKey, FPCCEvtApp, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; +import { FPCCEvtApp, convertToTokenAndClaims } from "@fireproof/cloud-connector-base"; export const clerkSvc = Lazy(async (dashApi: DashApi) => { const clerkPubKey = await dashApi.getClerkPublishableKey({}); @@ -14,16 +14,22 @@ export const clerkSvc = Lazy(async (dashApi: DashApi) => { await clerk.load(); clerk.addListener((session) => { if (session.user) { - dashApi.logger.Info().Any({ - user: session.user, - windowLocation: window.location.href, - clerkPubKey, - }).Msg("Svc Clerk-User signed in") + dashApi.logger + .Info() + .Any({ + user: session.user, + windowLocation: window.location.href, + clerkPubKey, + }) + .Msg("Svc Clerk-User signed in"); } else { - dashApi.logger.Info().Any({ - windowLocation: window.location.href, - clerkPubKey, - }).Msg("Svc Clerk-User signed out") + dashApi.logger + .Info() + .Any({ + windowLocation: window.location.href, + clerkPubKey, + }) + .Msg("Svc Clerk-User signed out"); } }); @@ -33,32 +39,33 @@ export const clerkSvc = Lazy(async (dashApi: DashApi) => { // const clerkFPCCEvtEntities = new KeyedResolvOnce(); export class ClerkFPCCEvtEntity implements BackendFPCC { - readonly appId: string; - readonly dbName: string; - readonly deviceId: string; + // readonly appId: string; + // readonly dbName: string; + // readonly deviceId: string; readonly sthis: SuperThis; readonly logger: Logger; readonly dashApi: DashApi; state: "needs-login" | "waiting" | "ready" = "needs-login"; - constructor(sthis: SuperThis, dashApi: DashApi, dbKey: DbKey, deviceId: string) { - this.logger = ensureLogger(sthis, `ClerkFPCCEvtEntity`, { - appId: dbKey.appId, - dbName: dbKey.dbName, - deviceId: deviceId, - }); - this.appId = dbKey.appId; - this.dbName = dbKey.dbName; - this.deviceId = deviceId; + constructor(sthis: SuperThis, dashApi: DashApi/*, dbKey: DbKey, deviceId: string*/) { + this.logger = ensureLogger(sthis, `ClerkFPCCEvtEntity`) + // , { + // appId: dbKey.appId, + // dbName: dbKey.dbName, + // deviceId: deviceId, + // }); + // this.appId = dbKey.appId; + // this.dbName = dbKey.dbName; + // this.deviceId = deviceId; this.sthis = sthis; this.dashApi = dashApi; } - async getCloudDbToken(auth: AuthType): Promise> { + async getCloudDbToken(auth: AuthType, bkey: BackendState): Promise> { const rRes = await this.dashApi.getCloudDbToken({ auth, - appId: this.appId, - localDbName: this.dbName, - deviceId: this.deviceId, + appId: bkey.appId, + localDbName: bkey.dbName, + deviceId: bkey.deviceId, }); if (rRes.isErr()) { return Result.Err(rRes); diff --git a/cloud/connector/svc/index.ts b/cloud/connector/svc/index.ts index 89b695163..228104b26 100644 --- a/cloud/connector/svc/index.ts +++ b/cloud/connector/svc/index.ts @@ -1,3 +1,3 @@ export * from "./clerk-fpcc-evt-entity.js"; export * from "./fp-cloud-connector.js"; -export * from "./svc-fpcc-protocol.js"; \ No newline at end of file +export * from "./svc-fpcc-protocol.js"; diff --git a/cloud/connector/svc/package.json b/cloud/connector/svc/package.json index f2c68a76b..25a96723f 100644 --- a/cloud/connector/svc/package.json +++ b/cloud/connector/svc/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/fireproof-storage/fireproof/issues" }, "dependencies": { - "@adviser/cement": "^0.4.62", + "@adviser/cement": "^0.4.63", "@clerk/clerk-js": "^5.102.1", "@fireproof/cloud-connector-base": "workspace:*", "@fireproof/core-protocols-dashboard": "workspace:*", diff --git a/cloud/connector/svc/svc-fpcc-protocol.ts b/cloud/connector/svc/svc-fpcc-protocol.ts index 39cb2bbbf..179a3ca4d 100644 --- a/cloud/connector/svc/svc-fpcc-protocol.ts +++ b/cloud/connector/svc/svc-fpcc-protocol.ts @@ -12,10 +12,10 @@ import { FPCCProtocol, FPCCProtocolBase, dbAppKey, - DbKey, + Ready, } from "@fireproof/cloud-connector-base"; import { SuperThis } from "@fireproof/core-types-base"; -import { BuildURI, KeyedResolvSeq, Lazy, Logger, Result, sleep } from "@adviser/cement"; +import { BuildURI, Keyed, KeyedResolvOnce, KeyedResolvSeq, Lazy, Logger, Result, sleep } from "@adviser/cement"; import { DashApi, AuthType, @@ -30,7 +30,7 @@ export interface SvcFPCCProtocolOpts { readonly dashboardURI: string; readonly cloudApiURI: string; readonly sthis?: SuperThis; - // readonly backend: BackendFPCC; + readonly backend?: BackendFPCC; } export type GetCloudDbTokenResult = @@ -41,33 +41,71 @@ export type GetCloudDbTokenResult = | { readonly res: ResCloudDbTokenNotBound; }; + +export interface BackendKey { + readonly appURL: string; + readonly appId: string; + readonly dbName: string; + readonly ledger?: string | undefined; + readonly tenant?: string | undefined; +} + +export type BackendStates = "needs-login" | "waiting" | "ready"; +export interface BackendState extends BackendKey { + getState(): BackendStates; + setState(state: BackendStates): BackendStates; + reset(): void; +} + + export interface BackendFPCC { - readonly appId: string; - readonly dbName: string; - readonly deviceId: string; + // readonly appId: string; + // readonly dbName: string; + // readonly deviceId: string; isFPCCEvtAppReady(): boolean; - getState(): "needs-login" | "waiting" | "ready"; - setState(state: "needs-login" | "waiting" | "ready"): "needs-login" | "waiting" | "ready"; waitForAuthToken(resultId: string): Promise>; getFPCCEvtApp(): Promise>; setFPCCEvtApp(app: FPCCEvtApp): Promise; isUserLoggedIn(): Promise; getDashApiToken(): Promise>; // listRegisteredDbNames(): Promise; - getCloudDbToken(auth: AuthType): Promise>; + getCloudDbToken(auth: AuthType, bkey: BackendKey): Promise>; } + // function getBackendFromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): BackendFPCC { // return ClerkFPCCEvtEntity.fromRegisterLocalDbName(sthis, dashApi, req, deviceId); // } -const registeredDbs = new Map(); +// const registeredDbs = new Map(); // static fromRegisterLocalDbName(sthis: SuperThis, dashApi: Api, req: DbKey, deviceId: string): ClerkFPCCEvtEntity { // const key = dbAppKey(req); // return clerkFPCCEvtEntities.get(key).once(() => new ClerkFPCCEvtEntity(sthis, dashApi, req, deviceId)); // } +const backendPerDashUri = new KeyedResolvOnce(); + +const backendStatePerDb = new Keyed(({key}: { key: BackendKey}) => { + let state: BackendStates = "needs-login"; + const hash = hashObjectSync({ + appURL: key.appURL, + appId: key.appId, + dbName: key.dbName, + ledger: key.ledger, + tenant: key.tenant, + }); + return { + ...key, + getState: () => state, + setState: (newState: BackendStates) => { state = newState; return state }, + key: hash, + reset: () => { + throw new Error("Not implemented yet"); + } + } +}, { +}) export class SvcFPCCProtocol implements FPCCProtocol { readonly sthis: SuperThis; @@ -77,6 +115,7 @@ export class SvcFPCCProtocol implements FPCCProtocol { readonly dashApiURI: string; readonly dashApi: DashApi; readonly hash: () => string; + readonly backendFPCC: BackendFPCC; constructor(sthis: SuperThis, opts: SvcFPCCProtocolOpts) { this.sthis = sthis; @@ -87,6 +126,9 @@ export class SvcFPCCProtocol implements FPCCProtocol { this.dashApiURI = opts.cloudApiURI ?? "https://dev.connect.fireproof.direct/api"; // console.log("IframeFPCCProtocol constructed with", opts); this.dashApi = new DashApi(this.sthis, this.dashApiURI); + this.backendFPCC = opts.backend ?? backendPerDashUri.get(this.dashboardURI).once(() => { + return new ClerkFPCCEvtEntity(this.sthis, this.dashApi); + }) } // readonly activeIframes = new KeyedResolvOnce>(); @@ -139,22 +181,26 @@ export class SvcFPCCProtocol implements FPCCProtocol { // }); // } - registeredDb(key: DbKey) { - const mapKey = dbAppKey(key); - const existing = registeredDbs.get(mapKey); - if (existing) { - return existing; - } - const newEntity = new ClerkFPCCEvtEntity(this.sthis, this.dashApi, key, this.getDeviceId()); - registeredDbs.set(mapKey, newEntity); - return newEntity; - } + // buildBackendKey(key: DbKey): BackendState { + // return { + // ...key, + // deviceId: this.getDeviceId(), + // } + // // const mapKey = dbAppKey(key); + // // const existing = registeredDbs.get(mapKey); + // // if (existing) { + // // return existing; + // // } + // // const newEntity = new ClerkFPCCEvtEntity(this.sthis, this.dashApi, key, this.getDeviceId()); + // // registeredDbs.set(mapKey, newEntity); + // // return newEntity; + // } getDeviceId(): string { return "we-need-to-implement-device-id"; } - async requestPageToDoLogin(backend: BackendFPCC, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { + async requestPageToDoLogin(bkey: BackendKey, event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { const loginTID = this.sthis.nextId(16).str; const url = BuildURI.from(this.dashboardURI) .setParam("back_url", "wait-for-token") // dummy back_url since we don't return to the app here @@ -178,26 +224,35 @@ export class SvcFPCCProtocol implements FPCCProtocol { reason: "BindCloud", }; - this.sendMessage(fpccEvtNeedsLogin, srcEvent); - backend.waitForAuthToken(loginTID).then((rAuthToken) => { + this.fpccProtocol.sendMessage(fpccEvtNeedsLogin, srcEvent); + this.backendFPCC.waitForAuthToken(loginTID).then((rAuthToken) => { if (rAuthToken.isErr()) { this.logger.Error().Err(rAuthToken).Msg("Failed to obtain auth token after login"); return; } - return backend + const bstate = backendStatePerDb.get(event).once(() => { + let state: BackendStates= "needs-login"; + return { + appId: event.appId, + dbName: event.dbName, + deviceId: this.getDeviceId(), + getState: () => state, + setState: (newState: BackendStates) => { + state = newState; + } + } + }); + return this.backendFPCC .getCloudDbToken({ type: "clerk", token: rAuthToken.Ok().token, - }) + }, bstate) .then((rCloudToken) => { if (rCloudToken.isErr()) { throw this.logger .Error() .Err(rCloudToken) - .Any({ - appId: backend.appId, - dbName: backend.dbName, - }) + .Any({...bstate}) .Msg("Failed to obtain DB token after login") .AsError(); } @@ -208,8 +263,7 @@ export class SvcFPCCProtocol implements FPCCProtocol { .Error() .Str("status", cloudToken.res.status) .Any({ - appId: backend.appId, - dbName: backend.dbName, + ...bstate }) .Msg("DB is still not bound after login") .AsError(); @@ -223,8 +277,7 @@ export class SvcFPCCProtocol implements FPCCProtocol { .Error() .Err(rTanc) .Any({ - appId: backend.appId, - dbName: backend.dbName, + ...bstate }) .Msg("Failed to convert DB token to token and claims after login"); } @@ -233,7 +286,7 @@ export class SvcFPCCProtocol implements FPCCProtocol { tid: event.tid, dst: event.src, type: "FPCCEvtApp", - appId: backend.appId, + appId: bstate.appId, appFavIcon: { defURL: "https://fireproof.direct/favicon.ico", }, @@ -245,36 +298,48 @@ export class SvcFPCCProtocol implements FPCCProtocol { iconURL: "https://fireproof.direct/favicon.ico", }, localDb: { - dbName: backend.dbName, + dbName: bstate.dbName, tenantId: claims.selected.tenant, ledgerId: claims.selected.ledger, accessToken: token, }, env: {}, // future env vars } satisfies FPCCSendMessage; - backend.setState("ready"); - backend.setFPCCEvtApp(this.sendMessage(fpccEvtApp, srcEvent)); + bstate.setState("ready"); + this.backendFPCC.setFPCCEvtApp(this.fpccProtocol.sendMessage(fpccEvtApp, srcEvent)); // this.logger.Info().Any(fpccEvtApp).Msg("Successfully obtained token for DB after login"); }); }); } readonly stateSeq = new KeyedResolvSeq(); - runStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { - return this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + runStateMachine(event: FPCCReqRegisterLocalDbName, srcEvent: MessageEvent): Promise { + // const bkey: BackendKey = { + // appId: event.appId, + // dbName: event.localDb.dbName, + // deviceId: this.getDeviceId(), + // }; + const key = dbAppKey(event); + return this.stateSeq.get(key).add(() => backendStatePerDb.get(key).this.atomicRunStateMachine(event, event, srcEvent)); } - listRegisteredDbs(): BackendFPCC[] { - return Array.from(registeredDbs.values()); + listRegisteredDbs(): BackendKey[] { + return Array.from(backendStatePerDb.values()).filter((bstate) => bstate.value.Ok().getState() === "ready").map((state) => { + const bstate = state.value.Ok(); + return { + appId: bstate.appId, + dbName: bstate.dbName, + deviceId: bstate.deviceId, + }; + }) } - async atomicRunStateMachine(backend: BackendFPCC, event: FPCCMessage, srcEvent: MessageEvent): Promise { - const bstate = backend.getState(); + async atomicRunStateMachine(state: BackendState, event: FPCCMessage, srcEvent: MessageEvent): Promise { + const bstate = state.getState(); switch (true) { case bstate === "ready" && isFPCCReqRegisterLocalDbName(event): this.logger.Debug().Msg("Backend is ready, sending FPCCEvtApp"); - return backend - .getFPCCEvtApp() + return this.backendFPCC.getFPCCEvtApp() .then((rFpccEvtApp) => { if (rFpccEvtApp.isOk()) { this.sendMessage(rFpccEvtApp.Ok(), srcEvent); @@ -292,33 +357,33 @@ export class SvcFPCCProtocol implements FPCCProtocol { break; case bstate === "needs-login" && isFPCCReqRegisterLocalDbName(event): { - this.logger.Debug().Msg("Backend needs login", backend.appId, backend.dbName); - const rAuthToken = await backend.getDashApiToken(); + this.logger.Debug().Msg("Backend needs login", state.appId, state.dbName); + const rAuthToken = await this.backendFPCC.getDashApiToken(); if (rAuthToken.isErr()) { this.logger .Warn() .Err(rAuthToken) - .Any({ appId: backend.appId, dbName: backend.dbName }) + .Any({ appId: state.appId, dbName: state.dbName }) .Msg("User not logged in, requesting login"); // make all dbs go to waiting state - backend.setState("waiting"); - return this.requestPageToDoLogin(backend, event, srcEvent); + state.setState("waiting"); + return this.requestPageToDoLogin(state, event, srcEvent); } else { // const backend = this.registeredDb(event); - if (backend.isFPCCEvtAppReady()) { - const rFpccEvtApp = await backend.getFPCCEvtApp(); + if (this.backendFPCC.isFPCCEvtAppReady()) { + const rFpccEvtApp = await this.backendFPCC.getFPCCEvtApp(); if (rFpccEvtApp.isErr()) { this.logger .Warn() .Err(rFpccEvtApp) - .Any({ appId: backend.appId, dbName: backend.dbName }) + .Any({ appId: state.appId, dbName: state.dbName }) .Msg("Backend reports error"); } if (rFpccEvtApp.isOk()) { this.logger .Debug() - .Any({ appId: backend.appId, dbName: backend.dbName, fpccEvtApp: rFpccEvtApp.Ok() }) + .Any({ appId: state.appId, dbName: state.dbName, fpccEvtApp: rFpccEvtApp.Ok() }) .Msg("Sending existing FPCCEvtApp"); this.sendMessage(rFpccEvtApp.Ok(), srcEvent); return; @@ -326,37 +391,37 @@ export class SvcFPCCProtocol implements FPCCProtocol { } else { const rDbToken = await this.dashApi.getCloudDbToken({ auth: rAuthToken.Ok(), - appId: backend.appId, - localDbName: backend.dbName, - deviceId: backend.deviceId, + appId: state.appId, + localDbName: state.dbName, + deviceId: state.deviceId, }); if (rDbToken.isErr()) { this.logger .Error() .Err(rDbToken) - .Any({ appId: backend.appId, dbName: backend.dbName }) + .Any({ appId: state.appId, dbName: state.dbName }) .Msg("Unexpected error obtaining DB token"); - backend.setState("waiting"); + state.setState("waiting"); await sleep(60000); - this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + this.stateSeq.get(dbAppKey(state)).add(() => this.atomicRunStateMachine(state, event, srcEvent)); return; } if (rDbToken.Ok().status === "not-bound") { - this.logger.Debug().Any({ appId: backend.appId, dbName: backend.dbName }).Msg("DB is not bound, requesting login"); + this.logger.Debug().Any({ appId: state.appId, dbName: state.dbName }).Msg("DB is not bound, requesting login"); // make all dbs go to waiting state - backend.setState("waiting"); - return this.requestPageToDoLogin(backend, event, srcEvent); + state.setState("waiting"); + return this.requestPageToDoLogin(state, event, srcEvent); } else { - const rCloudToken = await backend.getCloudDbToken(rAuthToken.Ok()); + const rCloudToken = await this.backendFPCC.getCloudDbToken(rAuthToken.Ok(), state); if (rCloudToken.isErr()) { this.logger.Warn().Err(rCloudToken).Msg("Failed to obtain DB token, re-running state machine after delay"); await sleep(1000); - this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + this.stateSeq.get(dbAppKey(state)).add(() => this.atomicRunStateMachine(state, event, srcEvent)); return; } const res = rCloudToken.Ok().res; if (!isResCloudDbTokenBound(res)) { - return this.requestPageToDoLogin(backend, event, srcEvent); + return this.requestPageToDoLogin(state, event, srcEvent); } const rTandC = await convertToTokenAndClaims(this.dashApi, this.logger, res.token); if (rTandC.isErr()) { @@ -365,7 +430,7 @@ export class SvcFPCCProtocol implements FPCCProtocol { .Err(rTandC) .Msg("Failed to convert DB token to token and claims, re-running state machine after delay"); await sleep(1000); - this.stateSeq.get(dbAppKey(backend)).add(() => this.atomicRunStateMachine(backend, event, srcEvent)); + this.stateSeq.get(dbAppKey(state)).add(() => this.atomicRunStateMachine(state, event, srcEvent)); return; } const { token, claims } = rTandC.Ok(); @@ -374,11 +439,11 @@ export class SvcFPCCProtocol implements FPCCProtocol { type: "FPCCEvtApp", src: "iframe", dst: event.src, - appId: backend.appId, + appId: state.appId, appFavIcon: { defURL: "https://fireproof.direct/favicon.ico", }, - devId: backend.deviceId, + devId: state.deviceId, user: { name: claims.nickname ?? claims.userId, email: claims.email, @@ -386,18 +451,18 @@ export class SvcFPCCProtocol implements FPCCProtocol { iconURL: "https://fireproof.direct/favicon.ico", }, localDb: { - dbName: backend.dbName, + dbName: state.dbName, tenantId: claims.selected.tenant, ledgerId: claims.selected.ledger, accessToken: token, }, env: {}, }; - await backend.setFPCCEvtApp(fpccEvtApp); - backend.setState("ready"); + await this.backendFPCC.setFPCCEvtApp(fpccEvtApp); + state.setState("ready"); this.logger .Debug() - .Any({ appId: backend.appId, dbName: backend.dbName }) + .Any({ appId: state.appId, dbName: state.dbName }) .Msg("Sent FPCCEvtApp after obtaining DB token"); this.sendMessage(fpccEvtApp, srcEvent); return; @@ -429,7 +494,7 @@ export class SvcFPCCProtocol implements FPCCProtocol { this.fpccProtocol.injectSend(sendFn); } - readonly ready = Lazy(async (): Promise => { + readonly ready = Lazy(async (): Promise> => { await clerkSvc(this.dashApi); await this.fpccProtocol.ready(); @@ -447,11 +512,13 @@ export class SvcFPCCProtocol implements FPCCProtocol { this.sendMessage(readyEvent, srcEvent); }) - this.fpccProtocol.onFPCCMessage(async (event, srcEvent: MessageEvent) => { - return this.runStateMachine(backend, event, srcEvent); + this.fpccProtocol.onFPCCReqRegisterLocalDbName(async (event, srcEvent: MessageEvent) => { + return this.runStateMachine(event, srcEvent); }); - return this; + return Result.Ok({ + type: "ready" + }); }); sendMessage(message: FPCCSendMessage, srcEvent: MessageEvent | string): T { diff --git a/cloud/connector/test/package.json b/cloud/connector/test/package.json index 5ac867d49..6d8d99437 100644 --- a/cloud/connector/test/package.json +++ b/cloud/connector/test/package.json @@ -37,9 +37,9 @@ "@fireproof/cloud-connector-svc": "workspace:*", "@fireproof/cloud-connector-page": "workspace:*", "@fireproof/core-runtime": "workspace:*", - "@vitest/browser": "^4.0.4", - "@vitest/browser-playwright": "^4.0.4", + "@vitest/browser": "^4.0.8", + "@vitest/browser-playwright": "^4.0.8", "ts-essentials": "^10.1.1", - "vitest": "^4.0.4" + "vitest": "^4.0.8" } } diff --git a/cloud/connector/test/page-fpcc-protocol.test.ts b/cloud/connector/test/page-fpcc-protocol.test.ts index 387233183..f9914fb4e 100644 --- a/cloud/connector/test/page-fpcc-protocol.test.ts +++ b/cloud/connector/test/page-fpcc-protocol.test.ts @@ -1,15 +1,29 @@ import { describe, expect, it, vi } from "vitest"; -import { PageFPCCProtocol } from "@fireproof/cloud-connector-page"; +import { FPCloudFrontend, pageFPCCProtocol } from "@fireproof/cloud-connector-page"; import { SvcFPCCProtocol } from "@fireproof/cloud-connector-svc"; -import { FPCCMessage, FPCCPing } from "@fireproof/cloud-connector-base"; +import { FPCCEvtNeedsLogin, FPCCMessage, FPCCPing } from "@fireproof/cloud-connector-base"; import { ensureSuperThis } from "@fireproof/core-runtime"; import { Writable } from "ts-essentials"; +export class TestFrontend implements FPCloudFrontend { + hash(): string { + return "test-frontend-hash"; + } + openLogin(_msg: FPCCEvtNeedsLogin): void { + // no-op + } + stop(): void { + // no-op + } +} + describe("FPCC Protocol", () => { const sthis = ensureSuperThis(); - const pageProtocol = new PageFPCCProtocol(sthis, { + const pageProtocol = pageFPCCProtocol({ + sthis, iframeHref: "https://example.com/iframe", loginWaitTime: 1000, + fpCloudFrontend: new TestFrontend(), }); const iframeProtocol = new SvcFPCCProtocol(sthis, { dashboardURI: "https://example.com/dashboard", @@ -19,7 +33,7 @@ describe("FPCC Protocol", () => { iframeProtocol.injectSend((evt: Writable) => { evt.src = evt.src ?? "iframe"; // console.log("IframeFPCCProtocol sending message", evt); - pageProtocol.handleMessage({ data: evt, origin: "iframe" } as MessageEvent); + pageProtocol.fpccProtocol.handleMessage({ data: evt, origin: "iframe" } as MessageEvent); return evt; }); @@ -43,9 +57,9 @@ describe("FPCC Protocol", () => { timestamp: Date.now(), }; const fpccFn = vi.fn(); - pageProtocol.onFPCCMessage(fpccFn); + pageProtocol.fpccProtocol.onFPCCMessage(fpccFn); await protocolStart(); - pageProtocol.sendMessage(pingMessage); + pageProtocol.fpccProtocol.sendMessage(pingMessage, "iframe"); expect(fpccFn.mock.calls[fpccFn.mock.calls.length - 1]).toEqual([ { dst: "page", diff --git a/core/protocols/dashboard/msg-api.ts b/core/protocols/dashboard/msg-api.ts index 28e661503..f9d5b0682 100644 --- a/core/protocols/dashboard/msg-api.ts +++ b/core/protocols/dashboard/msg-api.ts @@ -20,6 +20,8 @@ export class DashApi { this.apiUrl = apiUrl; } + readonly hash = Lazy(() => this.apiUrl); + async request(req: Q): Promise> { return exception2Result(async () => { const res = await fetch(this.apiUrl.toString(), { diff --git a/core/runtime/utils.ts b/core/runtime/utils.ts index e2ca56e81..1c7846202 100644 --- a/core/runtime/utils.ts +++ b/core/runtime/utils.ts @@ -73,7 +73,6 @@ class SuperThisImpl implements SuperThis { hash(): string { return "superthis-hash-is-not-implemented-but-a-dummy"; - } nextId(bytes = 6): { str: string; bin: Uint8Array } { diff --git a/core/tests/blockstore/standalone.test.ts b/core/tests/blockstore/standalone.test.ts index 8462165d4..405e9f217 100644 --- a/core/tests/blockstore/standalone.test.ts +++ b/core/tests/blockstore/standalone.test.ts @@ -2,7 +2,7 @@ import { BuildURI, runtimeFn, URI, sleep } from "@adviser/cement"; import { Link } from "multiformats"; import { stripper } from "@adviser/cement/utils"; import pLimit from "@fireproof/vendor/p-limit"; -import { ensureSuperThis, } from "@fireproof/core-runtime"; +import { ensureSuperThis } from "@fireproof/core-runtime"; import { CRDT, PARAM, LedgerOpts } from "@fireproof/core-types-base"; import { describe, it, vi, expect, beforeEach, afterEach } from "vitest"; import { Loader } from "@fireproof/core-blockstore"; diff --git a/core/tests/fireproof/attachable.test.ts b/core/tests/fireproof/attachable.test.ts index b5c9c9d87..b48242007 100644 --- a/core/tests/fireproof/attachable.test.ts +++ b/core/tests/fireproof/attachable.test.ts @@ -5,7 +5,7 @@ import { CarReader } from "@ipld/car/reader"; import * as dagCbor from "@ipld/dag-cbor"; import { mockLoader } from "../helpers.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ensureSuperThis, } from "@fireproof/core-runtime"; +import { ensureSuperThis } from "@fireproof/core-runtime"; import { DefSerdeGateway } from "@fireproof/core-gateways-base"; import { MemoryGateway } from "@fireproof/core-gateways-memory"; import { AttachedRemotesImpl } from "@fireproof/core-blockstore"; diff --git a/core/tests/fireproof/fireproof.test.ts b/core/tests/fireproof/fireproof.test.ts index a92ad0227..20cd6bd86 100644 --- a/core/tests/fireproof/fireproof.test.ts +++ b/core/tests/fireproof/fireproof.test.ts @@ -4,7 +4,7 @@ import { CID } from "multiformats/cid"; import { Index, index, fireproof, isDatabase } from "@fireproof/core-base"; import { URI, sleep } from "@adviser/cement"; -import { ensureSuperThis, } from "@fireproof/core-runtime"; +import { ensureSuperThis } from "@fireproof/core-runtime"; import { DocResponse, DocWithId, IndexRows, Database, PARAM, MapFn, ConfigOpts } from "@fireproof/core-types-base"; import { describe, afterEach, beforeEach, it, expect, beforeAll, assert } from "vitest"; import { AnyLink } from "@fireproof/core-types-blockstore"; diff --git a/dashboard/package.json b/dashboard/package.json index e88a923ca..18fc5fdef 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -30,6 +30,7 @@ "@clerk/clerk-js": "^5.107.0", "@clerk/clerk-react": "^5.54.0", "@fireproof/core": "workspace:0.0.0", + "@fireproof/core-base": "^0.23.15", "@fireproof/core-protocols-cloud": "workspace:0.0.0", "@fireproof/core-protocols-dashboard": "workspace:0.0.0", "@fireproof/core-runtime": "workspace:0.0.0", @@ -59,7 +60,6 @@ "@cloudflare/vite-plugin": "^1.14.1", "@cloudflare/workers-types": "^4.20251111.0", "@eslint/js": "^9.39.1", - "@fireproof/core-cli": "workspace:0.0.0", "@fireproof/cloud-connector-iframe": "workspace:0.0.0", "@fireproof/core-cli": "workspace:*", "@libsql/client": "^0.15.15", diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index b7b8c9c0a..dffe5e2e9 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -1,9 +1,5 @@ import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -// import { visualizer } from "rollup-plugin-visualizer"; -||||||| parent of 33350045 (chore: lets test the deployment) -import { defineConfig } from "vite"; -import { visualizer } from "rollup-plugin-visualizer"; +import { defineConfig, Plugin } from "vite"; import { dotenv } from "zx"; import { cloudflare } from "@cloudflare/vite-plugin"; import * as path from "path"; @@ -131,7 +127,6 @@ export default defineConfig({ }); }, }, ->>>>>>> 4cf9f42f (chore: intro of fp-cloud-connector) ], define: { ...defines(), diff --git a/eslint.config.mjs b/eslint.config.mjs index 4ed00b38a..d27c8cf2e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,6 +18,7 @@ const opts = tseslint.config( ignores: [ "babel.config.cjs", "jest.config.js", + "playwright-chrome/", "**/dist/", "**/pubdir/", "**/node_modules/", diff --git a/package.json b/package.json index 956ced001..ed0583845 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,8 @@ "workerd" ], "patchedDependencies": { - "drizzle-kit": "patches/drizzle-kit.patch" + "drizzle-kit": "patches/drizzle-kit.patch", + "@clerk/clerk-js": "patches/@clerk__clerk-js.patch" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b73408799..0f2828b7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false patchedDependencies: + '@clerk/clerk-js': + hash: b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e + path: patches/@clerk__clerk-js.patch drizzle-kit: hash: 9e79163b9304da5cbc3c787034937aeddaf678492ba5636df601baaa78e130d8 path: patches/drizzle-kit.patch @@ -423,7 +426,7 @@ importers: cloud/connector/base: dependencies: '@adviser/cement': - specifier: ^0.4.62 + specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) '@fireproof/core-runtime': specifier: workspace:* @@ -447,11 +450,11 @@ importers: cloud/connector/iframe: dependencies: '@adviser/cement': - specifier: ^0.4.62 + specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) '@clerk/clerk-js': specifier: ^5.102.0 - version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) + version: 5.107.0(patch_hash=b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e)(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../base @@ -471,7 +474,7 @@ importers: cloud/connector/page: dependencies: '@adviser/cement': - specifier: ^0.4.62 + specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) '@fireproof/cloud-connector-base': specifier: workspace:* @@ -489,11 +492,11 @@ importers: cloud/connector/svc: dependencies: '@adviser/cement': - specifier: ^0.4.62 + specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) '@clerk/clerk-js': specifier: ^5.102.1 - version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) + version: 5.107.0(patch_hash=b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e)(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) '@fireproof/cloud-connector-base': specifier: workspace:* version: link:../base @@ -531,17 +534,17 @@ importers: specifier: workspace:* version: link:../../../core/runtime '@vitest/browser': - specifier: ^4.0.4 - version: 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) + specifier: ^4.0.8 + version: 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8) '@vitest/browser-playwright': - specifier: ^4.0.4 - version: 4.0.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) + specifier: ^4.0.8 + version: 4.0.8(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8) ts-essentials: specifier: ^10.1.1 version: 10.1.1(typescript@5.9.3) vitest: - specifier: ^4.0.4 - version: 4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) + specifier: ^4.0.8 + version: 4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) cloud/todo-app: dependencies: @@ -1225,13 +1228,16 @@ importers: version: 2.21.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@clerk/clerk-js': specifier: ^5.107.0 - version: 5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) + version: 5.107.0(patch_hash=b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e)(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) '@clerk/clerk-react': specifier: ^5.54.0 version: 5.54.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@fireproof/core': specifier: workspace:0.0.0 version: link:../core/core + '@fireproof/core-base': + specifier: ^0.23.15 + version: 0.23.15(typescript@5.9.3) '@fireproof/core-protocols-cloud': specifier: workspace:0.0.0 version: link:../core/protocols/cloud @@ -1395,12 +1401,6 @@ importers: '@adviser/cement': specifier: ^0.4.63 version: 0.4.63(typescript@5.9.3) - '@fireproof/cloud-connector-base': - specifier: workspace:* - version: link:../cloud/connector/base - '@fireproof/cloud-connector-page': - specifier: workspace:* - version: link:../cloud/connector/page '@fireproof/core-base': specifier: workspace:0.0.0 version: link:../core/base @@ -1935,12 +1935,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1965,12 +1959,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1995,12 +1983,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -2025,12 +2007,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -2055,12 +2031,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -2085,12 +2055,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -2115,12 +2079,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -2145,12 +2103,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -2175,12 +2127,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -2205,12 +2151,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -2235,12 +2175,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -2265,12 +2199,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -2295,12 +2223,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -2325,12 +2247,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -2355,12 +2271,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -2385,12 +2295,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -2415,12 +2319,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -2433,12 +2331,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -2463,12 +2355,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -2481,12 +2367,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -2511,12 +2391,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -2529,12 +2403,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -2553,12 +2421,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -2583,12 +2445,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -2613,12 +2469,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -2643,12 +2493,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -2699,6 +2543,57 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fireproof/core-base@0.23.15': + resolution: {integrity: sha512-oOfkW9GGGahnf+ws+hN01U9TUW7cRi+1i5Jvh6B2Ewh6IXBhnMMQ3aYAT9EU8HwubyqlrTbMxLt/B0vnhBKWsQ==} + + '@fireproof/core-blockstore@0.23.15': + resolution: {integrity: sha512-plfM4zYvcpgD98C+C9wqwGtuQrFnELKBcPEY96Jw+6jw+UCWKxld9c3rLWBhpab/coAew2LM6UW1NVgMUO0tKA==} + + '@fireproof/core-gateways-base@0.23.15': + resolution: {integrity: sha512-qKpD1ci/+1GDawWQyM8KLe03VG/M3rpvVSdOTBuphDg2sosU1LjWMmgqsxXj+vHqtYi10jlbex2LsFrZCp5E3g==} + + '@fireproof/core-gateways-cloud@0.23.15': + resolution: {integrity: sha512-rXH+FF7P5E/7KA2Lr2iwtwPhR1ZhxGnJgFcN+7jRGWr9ZZCIwmYkmPZRgFsr/S/bEYhzy4nE1+1fm7VcoyH+1g==} + + '@fireproof/core-gateways-file-deno@0.23.15': + resolution: {integrity: sha512-k/f5etfhGl1jh5fDXDQaEPvZS2d7hAK4om0ZzECmClJN++iY/MyCEZeSbDOqo+vUrzyQ3KyW9dLAtOJzYhj4Qg==} + + '@fireproof/core-gateways-file-node@0.23.15': + resolution: {integrity: sha512-f0UfhiE/lIgKsi7cgHEbQQAOTgtyRE/kxqancEHim68HX9MWcxfwx4GGJwLjjb2F//AQuHdyze8lIrXLAWVWow==} + + '@fireproof/core-gateways-file@0.23.15': + resolution: {integrity: sha512-zmRsAZ4HppKre53dtLhpjbD/F7BAZ3fpvJ4fG+ziXJ1P/pPxPggIxzMAtmfAR8w6enmHaroLRGAh3WqRViq1zw==} + + '@fireproof/core-gateways-indexeddb@0.23.15': + resolution: {integrity: sha512-T2PzbU/jC1WygBN5SqJrF+JfKWeKc0/p7Udqi5jHG8O1WZBDdaGgrOagt8PtE1FoZ0DvrMxM+pDwmXTZMJpSqQ==} + + '@fireproof/core-gateways-memory@0.23.15': + resolution: {integrity: sha512-1ZiZxexg2ret5kJTWKRXc+Lk1tntZWf74irCXwLE3IBlkc11FLhSlGhAS6xJNtp1RkkUHMJZ30OjUgQxpOuJAg==} + + '@fireproof/core-keybag@0.23.15': + resolution: {integrity: sha512-4u+Ax/3HopPv+pENerE/LUt48MhLhe7NUR65+wez+K9ZrAmZeNxChUA7cWFgkQFE6qDwvOCnZptvn4quWLKyIQ==} + + '@fireproof/core-protocols-cloud@0.23.15': + resolution: {integrity: sha512-bT+hDKcS39DnFY2x79vdkNfRDG2KfWgSFYl2yvo1p9/qA2OmzPYQHbHgBbx5sIgHBhrIjm6QMQL/gPDaVlRFEQ==} + + '@fireproof/core-runtime@0.23.15': + resolution: {integrity: sha512-lYcoG4J2gMMio+FJKdi5Pm+xf2yFiRJ/xYEsCFuNVUxt8jKI+vQqah31w8ePDTh3EMqOPJB1oj3yN2Kd/bXKWw==} + + '@fireproof/core-types-base@0.23.15': + resolution: {integrity: sha512-Te513vn1zrzsqsMwYnaPKOG0AK8kk8rWOeuLriRurr4M+EO5R+05mBv4xVCDCVCjUMOzsuw4nj8UeCGFA5eIKQ==} + + '@fireproof/core-types-blockstore@0.23.15': + resolution: {integrity: sha512-kaPzqnckvuxtNK+R0Pk5RWBd7OTspGrl1+FtOLMM8SPB2U4tQJBPQ74E8z2Digu07zYais5xTOYM79bnHp0wFw==} + + '@fireproof/core-types-protocols-cloud@0.23.15': + resolution: {integrity: sha512-SNeJsq2FhNljwt/N8WsMGA06tAUC1qOfIzMeWpmKfEMSoDOe/vtNF0GQALF91gMVKph26h/OvwjnnnlZ6dvfZg==} + + '@fireproof/core-types-runtime@0.23.15': + resolution: {integrity: sha512-n9bJvxNFI22E/gN/P8ocTh6/JuqpNcwWUXrpfLZW3FDXJS/tmww65mzxhZTNOV0SjmzVw2EKeEB1FIClsrpfdw==} + + '@fireproof/vendor@0.23.15': + resolution: {integrity: sha512-cwbNOffgitbjhZd83Jwakl6Sqe9pdONNyacEbTtALnbrqmL1FgzOBaPXOy/htUKWz61Irm7go03zGNKe8IIQ6w==} + '@fireproof/vendor@3.0.0': resolution: {integrity: sha512-d2aoTkxWN9wYQ6pOafGxifxIijEvTpX3sBIJSaTrEqhorxRWdIZm8PC84kNV6qQqMr2GNfCUfgqn1mgR54M2rQ==} @@ -3477,45 +3372,20 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@vitest/browser-playwright@4.0.4': - resolution: {integrity: sha512-jGKnGZ5ZKXuwQ1Ldwll/rZxk3webz4gz3kvoTYX2NH2ASPiwFGck8D09Sf2wVjCuDqebPXXd69zUIt1o4yQ5tA==} - peerDependencies: - playwright: '*' - vitest: 4.0.4 - '@vitest/browser-playwright@4.0.8': resolution: {integrity: sha512-MUi0msIAPXcA2YAuVMcssrSYP/yylxLt347xyTC6+ODl0c4XQFs0d2AN3Pc3iTa0pxIGmogflUV6eogXpPbJeA==} peerDependencies: playwright: '*' vitest: 4.0.8 - '@vitest/browser@4.0.4': - resolution: {integrity: sha512-1ZXztcBtRd3maKliHzWbQohsyRjam0ws6OPRWNWfGxFUOHTlNBtDnJAm8z1x7IzVkZ6JcOAumHJAbxNJh4tkDw==} - peerDependencies: - vitest: 4.0.4 - '@vitest/browser@4.0.8': resolution: {integrity: sha512-oG6QJAR0d7S5SDnIYZwjxCj/a5fhbp9ZE7GtMgZn+yCUf4CxtqbBV6aXyg0qmn8nbUWT+rGuXL2ZB6qDBUjv/A==} peerDependencies: vitest: 4.0.8 - '@vitest/expect@4.0.4': - resolution: {integrity: sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==} - '@vitest/expect@4.0.8': resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==} - '@vitest/mocker@4.0.4': - resolution: {integrity: sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/mocker@4.0.8': resolution: {integrity: sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==} peerDependencies: @@ -3527,33 +3397,18 @@ packages: vite: optional: true - '@vitest/pretty-format@4.0.4': - resolution: {integrity: sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==} - '@vitest/pretty-format@4.0.8': resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==} - '@vitest/runner@4.0.4': - resolution: {integrity: sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==} - '@vitest/runner@4.0.8': resolution: {integrity: sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==} - '@vitest/snapshot@4.0.4': - resolution: {integrity: sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==} - '@vitest/snapshot@4.0.8': resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==} - '@vitest/spy@4.0.4': - resolution: {integrity: sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==} - '@vitest/spy@4.0.8': resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==} - '@vitest/utils@4.0.4': - resolution: {integrity: sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==} - '@vitest/utils@4.0.8': resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} @@ -4215,11 +4070,6 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -6062,40 +5912,6 @@ packages: yaml: optional: true - vitest@4.0.4: - resolution: {integrity: sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.4 - '@vitest/browser-preview': 4.0.4 - '@vitest/browser-webdriverio': 4.0.4 - '@vitest/ui': 4.0.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vitest@4.0.8: resolution: {integrity: sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6664,7 +6480,7 @@ snapshots: - react - react-dom - '@clerk/clerk-js@5.107.0(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12)': + '@clerk/clerk-js@5.107.0(patch_hash=b21e97952bd0811ee3373b4ce4f4c363cd8d1d7cf01ed705089e8eb2ef466f7e)(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12)': dependencies: '@base-org/account': 2.0.1(@types/react@19.2.3)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(zod@4.1.12) '@clerk/localizations': 3.27.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -6894,9 +6710,6 @@ snapshots: '@esbuild/aix-ppc64@0.19.12': optional: true - '@esbuild/aix-ppc64@0.25.10': - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -6909,9 +6722,6 @@ snapshots: '@esbuild/android-arm64@0.19.12': optional: true - '@esbuild/android-arm64@0.25.10': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true @@ -6924,9 +6734,6 @@ snapshots: '@esbuild/android-arm@0.19.12': optional: true - '@esbuild/android-arm@0.25.10': - optional: true - '@esbuild/android-arm@0.25.12': optional: true @@ -6939,9 +6746,6 @@ snapshots: '@esbuild/android-x64@0.19.12': optional: true - '@esbuild/android-x64@0.25.10': - optional: true - '@esbuild/android-x64@0.25.12': optional: true @@ -6954,9 +6758,6 @@ snapshots: '@esbuild/darwin-arm64@0.19.12': optional: true - '@esbuild/darwin-arm64@0.25.10': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true @@ -6969,9 +6770,6 @@ snapshots: '@esbuild/darwin-x64@0.19.12': optional: true - '@esbuild/darwin-x64@0.25.10': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true @@ -6984,9 +6782,6 @@ snapshots: '@esbuild/freebsd-arm64@0.19.12': optional: true - '@esbuild/freebsd-arm64@0.25.10': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -6999,9 +6794,6 @@ snapshots: '@esbuild/freebsd-x64@0.19.12': optional: true - '@esbuild/freebsd-x64@0.25.10': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true @@ -7014,9 +6806,6 @@ snapshots: '@esbuild/linux-arm64@0.19.12': optional: true - '@esbuild/linux-arm64@0.25.10': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true @@ -7029,9 +6818,6 @@ snapshots: '@esbuild/linux-arm@0.19.12': optional: true - '@esbuild/linux-arm@0.25.10': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true @@ -7044,9 +6830,6 @@ snapshots: '@esbuild/linux-ia32@0.19.12': optional: true - '@esbuild/linux-ia32@0.25.10': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true @@ -7059,9 +6842,6 @@ snapshots: '@esbuild/linux-loong64@0.19.12': optional: true - '@esbuild/linux-loong64@0.25.10': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true @@ -7074,9 +6854,6 @@ snapshots: '@esbuild/linux-mips64el@0.19.12': optional: true - '@esbuild/linux-mips64el@0.25.10': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true @@ -7089,9 +6866,6 @@ snapshots: '@esbuild/linux-ppc64@0.19.12': optional: true - '@esbuild/linux-ppc64@0.25.10': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true @@ -7104,9 +6878,6 @@ snapshots: '@esbuild/linux-riscv64@0.19.12': optional: true - '@esbuild/linux-riscv64@0.25.10': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true @@ -7119,9 +6890,6 @@ snapshots: '@esbuild/linux-s390x@0.19.12': optional: true - '@esbuild/linux-s390x@0.25.10': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true @@ -7134,18 +6902,12 @@ snapshots: '@esbuild/linux-x64@0.19.12': optional: true - '@esbuild/linux-x64@0.25.10': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.25.4': optional: true - '@esbuild/netbsd-arm64@0.25.10': - optional: true - '@esbuild/netbsd-arm64@0.25.12': optional: true @@ -7158,18 +6920,12 @@ snapshots: '@esbuild/netbsd-x64@0.19.12': optional: true - '@esbuild/netbsd-x64@0.25.10': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.25.4': optional: true - '@esbuild/openbsd-arm64@0.25.10': - optional: true - '@esbuild/openbsd-arm64@0.25.12': optional: true @@ -7182,18 +6938,12 @@ snapshots: '@esbuild/openbsd-x64@0.19.12': optional: true - '@esbuild/openbsd-x64@0.25.10': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.25.4': optional: true - '@esbuild/openharmony-arm64@0.25.10': - optional: true - '@esbuild/openharmony-arm64@0.25.12': optional: true @@ -7203,9 +6953,6 @@ snapshots: '@esbuild/sunos-x64@0.19.12': optional: true - '@esbuild/sunos-x64@0.25.10': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true @@ -7218,9 +6965,6 @@ snapshots: '@esbuild/win32-arm64@0.19.12': optional: true - '@esbuild/win32-arm64@0.25.10': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true @@ -7233,9 +6977,6 @@ snapshots: '@esbuild/win32-ia32@0.19.12': optional: true - '@esbuild/win32-ia32@0.25.10': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true @@ -7248,9 +6989,6 @@ snapshots: '@esbuild/win32-x64@0.19.12': optional: true - '@esbuild/win32-x64@0.25.10': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -7303,6 +7041,227 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fireproof/core-base@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/core-keybag': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-protocols-cloud': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@ipld/dag-cbor': 9.2.5 + '@web3-storage/pail': 0.6.2 + charwise: 3.0.1 + prolly-trees: 1.0.4 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@fireproof/core-blockstore@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-gateways-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-cloud': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-file': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-indexeddb': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-memory': 0.23.15(typescript@5.9.3) + '@fireproof/core-keybag': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@ipld/car': 5.4.2 + '@ipld/dag-cbor': 9.2.5 + '@ipld/dag-json': 10.2.5 + '@web3-storage/pail': 0.6.2 + multiformats: 13.4.1 + p-map: 7.0.3 + p-retry: 7.1.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@fireproof/core-gateways-base@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@ipld/dag-json': 10.2.5 + '@web3-storage/pail': 0.6.2 + transitivePeerDependencies: + - typescript + + '@fireproof/core-gateways-cloud@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-gateways-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-protocols-cloud': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-protocols-cloud': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + jose: 6.1.1 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@fireproof/core-gateways-file-deno@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@types/deno': 2.5.0 + '@types/node': 24.10.1 + transitivePeerDependencies: + - typescript + + '@fireproof/core-gateways-file-node@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@fireproof/core-gateways-file@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-gateways-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-file-deno': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-file-node': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@fireproof/core-gateways-indexeddb@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-gateways-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + idb: 8.0.3 + transitivePeerDependencies: + - typescript + + '@fireproof/core-gateways-memory@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-gateways-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@fireproof/core-keybag@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-gateways-file': 0.23.15(typescript@5.9.3) + '@fireproof/core-gateways-indexeddb': 0.23.15(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + jose: 6.1.1 + multiformats: 13.4.1 + zod: 4.1.12 + transitivePeerDependencies: + - typescript + + '@fireproof/core-protocols-cloud@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-protocols-cloud': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@types/ws': 8.18.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@fireproof/core-runtime@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@adviser/ts-xxhash': 1.0.2 + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-protocols-cloud': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + cborg: 4.3.0 + jose: 6.1.1 + multiformats: 13.4.1 + transitivePeerDependencies: + - typescript + + '@fireproof/core-types-base@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@web3-storage/pail': 0.6.2 + jose: 6.1.1 + multiformats: 13.4.1 + prolly-trees: 1.0.4 + zod: 4.1.12 + transitivePeerDependencies: + - typescript + + '@fireproof/core-types-blockstore@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-runtime': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + '@web3-storage/pail': 0.6.2 + multiformats: 13.4.1 + transitivePeerDependencies: + - typescript + + '@fireproof/core-types-protocols-cloud@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/core-types-base': 0.23.15(typescript@5.9.3) + '@fireproof/core-types-blockstore': 0.23.15(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + jose: 6.1.1 + multiformats: 13.4.1 + zod: 4.1.12 + transitivePeerDependencies: + - typescript + + '@fireproof/core-types-runtime@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + '@fireproof/vendor': 0.23.15(typescript@5.9.3) + multiformats: 13.4.1 + transitivePeerDependencies: + - typescript + + '@fireproof/vendor@0.23.15(typescript@5.9.3)': + dependencies: + '@adviser/cement': 0.4.63(typescript@5.9.3) + yocto-queue: 1.2.1 + transitivePeerDependencies: + - typescript + '@fireproof/vendor@3.0.0': dependencies: yocto-queue: 1.2.1 @@ -8047,19 +8006,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser-playwright@4.0.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4)': - dependencies: - '@vitest/browser': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) - '@vitest/mocker': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) - playwright: 1.56.1 - tinyrainbow: 3.0.3 - vitest: 4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser-playwright@4.0.8(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8)': dependencies: '@vitest/browser': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8) @@ -8073,23 +8019,6 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4)': - dependencies: - '@vitest/mocker': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) - '@vitest/utils': 4.0.4 - magic-string: 0.30.21 - pixelmatch: 7.1.0 - pngjs: 7.0.0 - sirv: 3.0.2 - tinyrainbow: 3.0.3 - vitest: 4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - ws: 8.18.3 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.8)': dependencies: '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) @@ -8107,15 +8036,6 @@ snapshots: - utf-8-validate - vite - '@vitest/expect@4.0.4': - dependencies: - '@standard-schema/spec': 1.0.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.0.4 - '@vitest/utils': 4.0.4 - chai: 6.2.0 - tinyrainbow: 3.0.3 - '@vitest/expect@4.0.8': dependencies: '@standard-schema/spec': 1.0.0 @@ -8125,14 +8045,6 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 4.0.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.8 @@ -8141,45 +8053,23 @@ snapshots: optionalDependencies: vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - '@vitest/pretty-format@4.0.4': - dependencies: - tinyrainbow: 3.0.3 - '@vitest/pretty-format@4.0.8': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.4': - dependencies: - '@vitest/utils': 4.0.4 - pathe: 2.0.3 - '@vitest/runner@4.0.8': dependencies: '@vitest/utils': 4.0.8 pathe: 2.0.3 - '@vitest/snapshot@4.0.4': - dependencies: - '@vitest/pretty-format': 4.0.4 - magic-string: 0.30.21 - pathe: 2.0.3 - '@vitest/snapshot@4.0.8': dependencies: '@vitest/pretty-format': 4.0.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.4': {} - '@vitest/spy@4.0.8': {} - '@vitest/utils@4.0.4': - dependencies: - '@vitest/pretty-format': 4.0.4 - tinyrainbow: 3.0.3 - '@vitest/utils@4.0.8': dependencies: '@vitest/pretty-format': 4.0.8 @@ -8893,35 +8783,6 @@ snapshots: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 - esbuild@0.25.10: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -10790,7 +10651,7 @@ snapshots: tsx@4.20.5: dependencies: - esbuild: 0.25.10 + esbuild: 0.25.12 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -10951,45 +10812,6 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 - vitest@4.0.4(@types/node@24.10.1)(@vitest/browser-playwright@4.0.4)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1): - dependencies: - '@vitest/expect': 4.0.4 - '@vitest/mocker': 4.0.4(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.4 - '@vitest/runner': 4.0.4 - '@vitest/snapshot': 4.0.4 - '@vitest/spy': 4.0.4 - '@vitest/utils': 4.0.4 - debug: 4.4.3 - es-module-lexer: 1.7.0 - expect-type: 1.2.2 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.10.1 - '@vitest/browser-playwright': 4.0.4(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1))(vitest@4.0.4) - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@4.0.8(@types/node@24.10.1)(@vitest/browser-playwright@4.0.8)(jiti@1.21.7)(tsx@4.20.5)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.8 diff --git a/use-fireproof/fp-cloud-connect-strategy-impl.ts b/use-fireproof/fp-cloud-connect-strategy-impl.ts index 70d0f8592..8cee74d81 100644 --- a/use-fireproof/fp-cloud-connect-strategy-impl.ts +++ b/use-fireproof/fp-cloud-connect-strategy-impl.ts @@ -7,7 +7,6 @@ import { FPCCEvtApp, dbAppKey } from "@fireproof/cloud-connector-base"; import { FPCloudConnectOpts, PageControllerImpl } from "./fp-cloud-connect-strategy.js"; import { FPCloudFrontendImpl } from "./window-open-fp-cloud.js"; - const registerLocalDbNames = new KeyedResolvOnce, string>(); export class FPCloudConnectStrategyImpl implements TokenStrategie { @@ -20,7 +19,6 @@ export class FPCloudConnectStrategyImpl implements TokenStrategie { readonly pageController: PageControllerImpl; constructor(opts: Partial) { - const dashboardURI = opts.dashboardURI ?? "https://dev.connect.fireproof.direct/"; let fpCloudConnectURL: BuildURI; if (opts.fpCloudConnectURL) { @@ -46,7 +44,7 @@ export class FPCloudConnectStrategyImpl implements TokenStrategie { sthis: this.sthis, iframeHref: this.fpCloudConnectURL, logger: this.logger, - frontend: opts.frontend ?? new FPCloudFrontendImpl(opts), + fpCloudFrontend: opts.fpCloudFrontend ?? new FPCloudFrontendImpl(opts), }); } readonly hash = Lazy(() => diff --git a/use-fireproof/fp-cloud-connect-strategy.ts b/use-fireproof/fp-cloud-connect-strategy.ts index 85fbff39a..47d632936 100644 --- a/use-fireproof/fp-cloud-connect-strategy.ts +++ b/use-fireproof/fp-cloud-connect-strategy.ts @@ -1,7 +1,7 @@ import { Future, KeyedResolvOnce, Lazy, Logger, poller, ResolveSeq, sleep } from "@adviser/cement"; import { SuperThis } from "@fireproof/core-types-base"; import { TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { ensureSuperThis, hashObjectSync, } from "@fireproof/core-runtime"; +import { ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; import { FPCCProtocol, FPCCProtocolBase, isInIframe } from "@fireproof/cloud-connector-base"; @@ -9,7 +9,7 @@ import { useEffect, useState } from "react"; import { defaultFPCloudConnectorOpts, fpCloudConnector } from "../cloud/connector/svc/fp-cloud-connector.js"; import { FPCloudConnectStrategyImpl } from "./fp-cloud-connect-strategy-impl.js"; import { FPCloudFrontend, initializeIframe, PageFPCCProtocolOpts } from "@fireproof/cloud-connector-page"; -import { FPCloudFrontendImpl} from "./window-open-fp-cloud.js"; +import { FPCloudFrontendImpl } from "./window-open-fp-cloud.js"; export interface FPCloudConnectOpts extends RedirectStrategyOpts { readonly dashboardURI?: string; @@ -18,7 +18,7 @@ export interface FPCloudConnectOpts extends RedirectStrategyOpts { readonly pageController: PageControllerImpl; readonly title?: string; readonly sthis?: SuperThis; - readonly frontend?: FPCloudFrontend; + readonly fpCloudFrontend?: FPCloudFrontend; } // which cases exist @@ -35,7 +35,7 @@ interface PageControllerOpts { readonly window: Window; readonly sthis: SuperThis; readonly logger: Logger; - readonly frontend: FPCloudFrontend; + // readonly frontend: FPCloudFrontend; } export type PageControllerImplOpts = PageFPCCProtocolOpts & Partial; @@ -81,7 +81,7 @@ export class PageControllerImpl { readonly sthis: SuperThis; readonly intervalMs: number; readonly iframeHref: string; - readonly frontend: FPCloudFrontend; + readonly fpCloudFrontend: FPCloudFrontend; constructor(opts: PageControllerImplOpts) { this.window = opts.window ?? window; @@ -90,9 +90,11 @@ export class PageControllerImpl { this.intervalMs = opts.intervalMs || 150; this.protocol = new FPCCProtocolBase(this.sthis, opts.logger); this.iframeHref = opts.iframeHref; - this.frontend = opts.frontend ?? new FPCloudFrontendImpl({ - sthis: this.sthis, - }); + this.fpCloudFrontend = + opts.fpCloudFrontend ?? + new FPCloudFrontendImpl({ + sthis: this.sthis, + }); } hash = Lazy(() => @@ -125,8 +127,8 @@ export class PageControllerImpl { this.openloginSeq.add(async () => { // test if all dbs are ready console.log("FPCloudConnectStrategy detected needs login event"); - this.frontend.openFireproofLogin(msg); - return; + this.fpCloudFrontend.openLogin(msg); + return; }); // logger.Info().Msg("FPCloudConnectStrategy detected needs login event"); }); @@ -136,8 +138,6 @@ export class PageControllerImpl { }); }); - - async #waitingForReady(tid: string, dst: string): Promise { const ready = new Future(); const unreg = this.protocol.onFPCCEvtConnectorReady((msg, _srcEvent) => { @@ -235,7 +235,6 @@ export function FPCloudConnectStrategy(opts: Partial = {}): }); } - // this is the backend fp service connector export function useFPCloudConnectSvc(): { fpSvc: FPCCProtocol; state: string } { const fpSvc = fpCloudConnector( diff --git a/use-fireproof/iframe-fp-cloud-connect-strategy.ts b/use-fireproof/iframe-fp-cloud-connect-strategy.ts deleted file mode 100644 index 27c64444e..000000000 --- a/use-fireproof/iframe-fp-cloud-connect-strategy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Lazy, Logger, Result } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core-types-base"; -import { ToCloudOpts, TokenAndSelectedTenantAndLedger, TokenStrategie } from "@fireproof/core-types-protocols-cloud"; -import { hashObjectSync } from "@fireproof/core-runtime"; - -import { defaultFPCloudConnectorOpts, fpCloudConnector } from "../cloud/connector/svc/fp-cloud-connector.js"; -import { SvcFPCCProtocol } from "../cloud/connector/svc/index.js"; - -class IframeFPCloudConnectStrategy implements TokenStrategie { - waitState: "started" | "stopped" = "stopped"; - readonly svc: SvcFPCCProtocol; - readonly hash: () => string; - - constructor(opts: Partial = {}) { - this.svc = fpCloudConnector(defaultFPCloudConnectorOpts(opts)); - this.hash = Lazy(() => hashObjectSync(opts)); - } - - readonly waitForIframes = Lazy((callback: (iframe: HTMLIFrameElement) => void) => { - // Check existing iframes first - document.querySelectorAll("iframe").forEach((iframe) => { - callback(iframe as HTMLIFrameElement); - }); - - // Watch for new iframes - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeName === "IFRAME") { - callback(node as HTMLIFrameElement); - } - - // Check if added node contains iframes - if (node instanceof Element) { - node.querySelectorAll("iframe").forEach((iframe) => { - callback(iframe as HTMLIFrameElement); - }); - } - }); - }); - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - return observer; // Return so you can disconnect later - }); - - ready(): Promise { - return this.svc.ready().then(() => { - this.waitForIframes((iframe) => { - this.svc.serveIframe(iframe); - }); - this.waitState = "started"; - }); - } - - open(_sthis: SuperThis, _logger: Logger, _localDbName: string, _opts: ToCloudOpts): void { - throw new Error("Method not implemented."); - } - waitForToken( - _sthis: SuperThis, - _logger: Logger, - _localDbName: string, - _opts: ToCloudOpts, - ): Promise> { - throw new Error("Method not implemented."); - } - stop(): void { - throw new Error("Method not implemented."); - } -} diff --git a/use-fireproof/package.json b/use-fireproof/package.json index 9f0c44106..94ebd7631 100644 --- a/use-fireproof/package.json +++ b/use-fireproof/package.json @@ -31,8 +31,6 @@ "@fireproof/core-types-base": "workspace:0.0.0", "@fireproof/core-types-blockstore": "workspace:0.0.0", "@fireproof/core-types-protocols-cloud": "workspace:0.0.0", - "@fireproof/cloud-connector-base": "workspace:*", - "@fireproof/cloud-connector-page": "workspace:*", "@fireproof/vendor": "workspace:0.0.0", "dompurify": "^3.3.0", "jose": "^6.1.1", diff --git a/use-fireproof/window-open-fp-cloud.ts b/use-fireproof/window-open-fp-cloud.ts index 815e6c1c1..3de76f4d3 100644 --- a/use-fireproof/window-open-fp-cloud.ts +++ b/use-fireproof/window-open-fp-cloud.ts @@ -7,7 +7,6 @@ import { SuperThis } from "@fireproof/core-types-base"; import { RedirectStrategyOpts } from "./redirect-strategy.js"; import { FPCloudFrontend } from "@fireproof/cloud-connector-page"; - export interface FPCloudFrontendImplOpts extends RedirectStrategyOpts { readonly sthis: SuperThis; readonly title: string; @@ -45,7 +44,7 @@ export class FPCloudFrontendImpl implements FPCloudFrontend { this.overlayNode = undefined; } - openFireproofLogin(msg: FPCCEvtNeedsLogin): void { + openLogin(msg: FPCCEvtNeedsLogin): void { // const redirectCtx = opts.context.get(WebCtx) as WebToCloudCtx; this.logger.Debug().Url(msg.loginURL).Msg("open redirect"); @@ -97,6 +96,5 @@ export class FPCloudFrontendImpl implements FPCloudFrontend { this.title, `left=${left},top=${top},width=${width},height=${height},scrollbars=yes,resizable=yes,popup=yes`, ); - } }