diff --git a/.gitignore b/.gitignore
index 05a79cfca..89a485382 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
# Files
+.claude/
.vscode
.idea
.DS_Store
@@ -23,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/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/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/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..2610b034e 100644
--- a/cloud/3rd-party/src/App.tsx
+++ b/cloud/3rd-party/src/App.tsx
@@ -1,22 +1,17 @@
-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: 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",
}),
- // 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/cloud/3rd-party/src/main.tsx b/cloud/3rd-party/src/main.tsx
index 854ee1408..dfc003786 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
new file mode 100644
index 000000000..2c9b83bfc
--- /dev/null
+++ b/cloud/3rd-party/src/overlayHtml.tsx
@@ -0,0 +1,22 @@
+import { jsx } from "use-fireproof";
+
+const { renderToString, React } = jsx;
+
+// 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(
+ ,
+ );
+}
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 1355a1f23..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",
@@ -13,6 +17,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/cloud/backend/base/ws-sockets.test.ts b/cloud/backend/base/ws-sockets.test.ts
index 7f0d73b30..d2a092677 100644
--- a/cloud/backend/base/ws-sockets.test.ts
+++ b/cloud/backend/base/ws-sockets.test.ts
@@ -1,8 +1,7 @@
-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";
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..98f0ed990
--- /dev/null
+++ b/cloud/connector/base/convert-to-token-and-claims.ts
@@ -0,0 +1,40 @@
+import { Logger, exception2Result, Result } from "@adviser/cement";
+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[] }>;
+ },
+ 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).Any({ inObj: rUnknownClaims.Ok().payload }).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/cloud/connector/base/fpcc-protocol.ts b/cloud/connector/base/fpcc-protocol.ts
new file mode 100644
index 000000000..812b038e7
--- /dev/null
+++ b/cloud/connector/base/fpcc-protocol.ts
@@ -0,0 +1,225 @@
+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, 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;
+
+ sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent): void;
+ handleError: (error: unknown) => void;
+ injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void;
+ ready(): Promise>;
+ stop(): void;
+}
+
+export class FPCCProtocolBase implements FPCCProtocol {
+ protected readonly sthis: SuperThis;
+ protected readonly logger: Logger;
+ readonly onStartFns: (() => void)[] = [];
+ #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) => {
+ 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.onFPCCMessage.invoke(fpCCmsg.data, event);
+ } else {
+ this.logger.Warn().Err(fpCCmsg.error).Any("event", event).Msg("Received non-FPCC message");
+ }
+ };
+
+ #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): {
+ 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(
+ Result.Ok({
+ type: "ready" as const,
+ }),
+ );
+ }
+
+ injectSend(sendFn: (msg: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void {
+ this.#sendFn = sendFn;
+ }
+
+ stop(): void {
+ this.#sendFn = undefined;
+ this.onFPCCMessage.clear();
+ this.onStartFns.splice(0, this.onStartFns.length);
+ }
+
+ sendMessage(msg: FPCCSendMessage, srcEvent: MessageEvent | string): T {
+ if (!this.#sendFn) {
+ throw new Error("Protocol not started. Call start() before sending messages.");
+ }
+ return this.#sendFn(
+ {
+ ...msg,
+ src: msg.src,
+ tid: msg.tid ?? this.sthis.nextId().str,
+ } as FPCCMessage,
+ srcEvent,
+ ) as T;
+ }
+}
+
+// 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/cloud/connector/base/index.ts b/cloud/connector/base/index.ts
new file mode 100644
index 000000000..91ee4064a
--- /dev/null
+++ b/cloud/connector/base/index.ts
@@ -0,0 +1,32 @@
+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 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 {
+ 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
new file mode 100644
index 000000000..03ef72486
--- /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.63",
+ "@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/cloud/connector/base/post-messager.ts b/cloud/connector/base/post-messager.ts
new file mode 100644
index 000000000..c2a9ea6a8
--- /dev/null
+++ b/cloud/connector/base/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 { SuperThis } from "@fireproof/core-types-base";
+import { Writable } from "ts-essentials";
+
+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/cloud/connector/base/protocol-fp-cloud-conn.ts b/cloud/connector/base/protocol-fp-cloud-conn.ts
new file mode 100644
index 000000000..6a17f6f00
--- /dev/null
+++ b/cloud/connector/base/protocol-fp-cloud-conn.ts
@@ -0,0 +1,191 @@
+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(),
+ 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();
+
+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;
+
+// FPCCReqRegisterLocalDbName schema
+export const FPCCReqRegisterLocalDbNameSchema = FPCCMsgBaseSchemaBase.extend({
+ type: z.literal("FPCCReqRegisterLocalDbName"),
+ appURL: z.string(),
+ appId: z.string(),
+ dbName: z.string(), // localDbName
+ ledger: z.string().optional(),
+ tenant: z.string().optional(),
+}).readonly();
+
+export type FPCCReqRegisterLocalDbName = 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", "unknown"]),
+ iconURL: 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();
+
+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;
+
+// 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,
+ FPCCReqRegisterLocalDbNameSchema,
+ FPCCEvtAppSchema,
+ FPCCPingSchema,
+ FPCCPongSchema,
+ FPCCEvtConnectorReadySchema,
+ FPCCReqWaitConnectorReadySchema,
+]);
+
+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 isFPCCReqRegisterLocalDbName(msg: FPCCMessage): msg is FPCCReqRegisterLocalDbName {
+ return msg.type === "FPCCReqRegisterLocalDbName";
+}
+
+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";
+}
+
+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/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/injected-iframe.html b/cloud/connector/iframe/injected-iframe.html
new file mode 100644
index 000000000..0b0688262
--- /dev/null
+++ b/cloud/connector/iframe/injected-iframe.html
@@ -0,0 +1,12 @@
+
+
+ Fireproof Cloud Connector
+
+
+ I'm the Fireproof Cloud Connector
+
+
+
diff --git a/cloud/connector/iframe/package.json b/cloud/connector/iframe/package.json
new file mode 100644
index 000000000..1262fb116
--- /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.63",
+ "@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-types-protocols-cloud": "workspace:*"
+ }
+}
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/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..f84d0a92d
--- /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.63",
+ "@fireproof/cloud-connector-base": "workspace:*",
+ "@fireproof/core-runtime": "workspace:*",
+ "@fireproof/core-types-base": "workspace:*",
+ "ts-essentials": "^10.1.1"
+ }
+}
diff --git a/cloud/connector/page/page-fpcc-protocol.ts b/cloud/connector/page/page-fpcc-protocol.ts
new file mode 100644
index 000000000..3edbabcb1
--- /dev/null
+++ b/cloud/connector/page/page-fpcc-protocol.ts
@@ -0,0 +1,315 @@
+import { ensureLogger, ensureSuperThis, hashObjectSync } from "@fireproof/core-runtime";
+import { SuperThis } from "@fireproof/core-types-base";
+import { Future, KeyedResolvOnce, Lazy, Logger, poller, ResolveOnce, Result, timeouted } from "@adviser/cement";
+import {
+ FPCCProtocol,
+ FPCCProtocolBase,
+ FPCCEvtApp,
+ FPCCMessage,
+ FPCCMsgBase,
+ FPCCReqRegisterLocalDbName,
+ FPCCReqWaitConnectorReady,
+ FPCCSendMessage,
+ 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 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 {
+ readonly register: FPCCReqRegisterLocalDbName;
+ readonly fpccEvtApp: FPCCEvtApp;
+}
+
+class PageFPCCProtocol implements FPCCProtocol {
+ // readonly sthis: SuperThis;
+ readonly logger: Logger;
+ readonly fpccProtocol: FPCCProtocolBase;
+ readonly maxConnectRetries: number;
+ readonly iFrameHref: string;
+
+ // readonly futureConnected = new Future();
+ readonly registerFPCCEvtApp = new KeyedResolvOnce();
+ // 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 = 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.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.registerFPCCEvtApp.reset();
+ this.starter.reset();
+ }
+
+ getAppId(): string {
+ // setup in ready
+ return "we-need-to-implement-app-id-this";
+ }
+
+ readonly handleError = (_error: unknown): void => {
+ throw new Error("Method not implemented.");
+ };
+
+ async registerDatabase(dbName: string, ireg: Partial = {}): Promise> {
+ return this.ready().then((peer) => {
+ if (!isPeerReady(peer)) {
+ return Result.Err(new Error("FPCC Protocol not ready - cannot register database"));
+ }
+ const sreg = {
+ ...ireg,
+ type: "FPCCReqRegisterLocalDbName",
+ appId: ireg.appId ?? this.getAppId(),
+ appURL: ireg.appURL ?? window.location.href,
+ dbName,
+ dst: ireg.dst ?? peer.peer,
+ } satisfies FPCCSendMessage;
+ const key = dbAppKey(sreg);
+ return this.registerFPCCEvtApp
+ .get(key)
+ .once(async () => {
+ const tid = this.sthis.nextId(12).str;
+ const fpccEvtAppFuture = new Future();
+ 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)),
+ {
+ 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);
+ }
+ return Result.Ok({
+ register: reg,
+ fpccEvtApp: rFPCCEvtApp.unwrap(),
+ } satisfies WaitForFPCCEvtApp);
+ })
+ .then((rWaitForFPCCEvtApp) => {
+ if (rWaitForFPCCEvtApp.isErr()) {
+ return Result.Err(rWaitForFPCCEvtApp);
+ }
+ return Result.Ok(rWaitForFPCCEvtApp.unwrap().fpccEvtApp);
+ });
+ });
+ }
+
+ injectSend(send: (evt: FPCCMessage, srcEvent: MessageEvent | string) => FPCCMessage): void {
+ this.fpccProtocol.injectSend(send);
+ }
+
+ sendMessage(event: FPCCSendMessage, srcEvent: MessageEvent | string): T {
+ return this.fpccProtocol.sendMessage(event, srcEvent);
+ }
+
+ 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();
+ });
+
+ const abortController = new AbortController();
+ const polling = poller(
+ async () => {
+ this.sendMessage(
+ {
+ type: "FPCCReqWaitConnectorReady",
+ tid,
+ src: `page-${window.location.href}-${this.hash()}`,
+ dst,
+ appId: this.getAppId(),
+ timestamp: Date.now(),
+ 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),
+ );
+ }
+ }
+ }),
+ );
+ } 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");
+ }
+
+ 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
new file mode 100644
index 000000000..3c76c38af
--- /dev/null
+++ b/cloud/connector/page/page-handler.ts
@@ -0,0 +1,71 @@
+/**
+ * Consumer program that creates and inserts an iframe with in-iframe.ts
+ */
+
+import { Future } from "@adviser/cement";
+import { Writable } from "ts-essentials";
+import { FPCCMessage, FPCCProtocolBase } from "@fireproof/cloud-connector-base";
+
+/**
+ * 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
+ */
+export function initializeIframe(pageProtocol: FPCCProtocolBase, iframeSrc: string): Promise {
+ (globalThis as Record)[Symbol.for("FP_PRESET_ENV")] = {
+ FP_DEBUG: "*",
+ };
+
+ const iframe = createIframe(iframeSrc);
+ const waitForLoad = new Future();
+ // Add load event listener
+ // console.log("Initializing FPCC iframe with src:", iframeHref.toString());
+ iframe.addEventListener("load", () => {
+ 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);
+ insertIframeAsLastElement(iframe);
+ return waitForLoad.asPromise().then(() => iframe);
+}
+
+// Initialize when script loads
+// initializeIframe();
+
+// export { createIframe, insertIframeAsLastElement, initializeIframe };
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/svc/clerk-fpcc-evt-entity.ts b/cloud/connector/svc/clerk-fpcc-evt-entity.ts
new file mode 100644
index 000000000..5ee34b1ae
--- /dev/null
+++ b/cloud/connector/svc/clerk-fpcc-evt-entity.ts
@@ -0,0 +1,215 @@
+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, 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 { 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, bkey: BackendState): Promise> {
+ const rRes = await this.dashApi.getCloudDbToken({
+ auth,
+ appId: bkey.appId,
+ localDbName: bkey.dbName,
+ deviceId: bkey.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..228104b26
--- /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";
diff --git a/cloud/connector/svc/package.json b/cloud/connector/svc/package.json
new file mode 100644
index 000000000..25a96723f
--- /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.63",
+ "@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..179a3ca4d
--- /dev/null
+++ b/cloud/connector/svc/svc-fpcc-protocol.ts
@@ -0,0 +1,529 @@
+import { ensureLogger, hashObjectSync } from "@fireproof/core-runtime";
+import {
+ convertToTokenAndClaims,
+ FPCCEvtApp,
+ FPCCEvtConnectorReady,
+ FPCCEvtNeedsLogin,
+ FPCCMessage,
+ FPCCMsgBase,
+ FPCCReqRegisterLocalDbName,
+ FPCCSendMessage,
+ isFPCCReqRegisterLocalDbName,
+ FPCCProtocol,
+ FPCCProtocolBase,
+ dbAppKey,
+ Ready,
+} from "@fireproof/cloud-connector-base";
+import { SuperThis } from "@fireproof/core-types-base";
+import { BuildURI, Keyed, KeyedResolvOnce, 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 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;
+ isFPCCEvtAppReady(): boolean;
+ waitForAuthToken(resultId: string): Promise>;
+ getFPCCEvtApp(): Promise>;
+ setFPCCEvtApp(app: FPCCEvtApp): Promise;
+ isUserLoggedIn(): Promise;
+ getDashApiToken(): Promise>;
+ // listRegisteredDbNames(): 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();
+
+// 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;
+ readonly logger: Logger;
+ readonly fpccProtocol: FPCCProtocolBase;
+ readonly dashboardURI: string;
+ readonly dashApiURI: string;
+ readonly dashApi: DashApi;
+ readonly hash: () => string;
+ readonly backendFPCC: BackendFPCC;
+
+ 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);
+ this.backendFPCC = opts.backend ?? backendPerDashUri.get(this.dashboardURI).once(() => {
+ return new ClerkFPCCEvtEntity(this.sthis, this.dashApi);
+ })
+ }
+
+ // 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);
+ // });
+ // });
+ // }
+
+ // 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(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
+ .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.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;
+ }
+ 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({...bstate})
+ .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({
+ ...bstate
+ })
+ .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({
+ ...bstate
+ })
+ .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: bstate.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: bstate.dbName,
+ tenantId: claims.selected.tenant,
+ ledgerId: claims.selected.ledger,
+ accessToken: token,
+ },
+ env: {}, // future env vars
+ } satisfies FPCCSendMessage;
+ 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(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(): 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(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 this.backendFPCC.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", state.appId, state.dbName);
+ const rAuthToken = await this.backendFPCC.getDashApiToken();
+ if (rAuthToken.isErr()) {
+ this.logger
+ .Warn()
+ .Err(rAuthToken)
+ .Any({ appId: state.appId, dbName: state.dbName })
+ .Msg("User not logged in, requesting login");
+ // make all dbs go to waiting state
+ state.setState("waiting");
+ return this.requestPageToDoLogin(state, event, srcEvent);
+ } else {
+ // const backend = this.registeredDb(event);
+
+ if (this.backendFPCC.isFPCCEvtAppReady()) {
+ const rFpccEvtApp = await this.backendFPCC.getFPCCEvtApp();
+ if (rFpccEvtApp.isErr()) {
+ this.logger
+ .Warn()
+ .Err(rFpccEvtApp)
+ .Any({ appId: state.appId, dbName: state.dbName })
+ .Msg("Backend reports error");
+ }
+ if (rFpccEvtApp.isOk()) {
+ this.logger
+ .Debug()
+ .Any({ appId: state.appId, dbName: state.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: state.appId,
+ localDbName: state.dbName,
+ deviceId: state.deviceId,
+ });
+ if (rDbToken.isErr()) {
+ this.logger
+ .Error()
+ .Err(rDbToken)
+ .Any({ appId: state.appId, dbName: state.dbName })
+ .Msg("Unexpected error obtaining DB token");
+ state.setState("waiting");
+ await sleep(60000);
+ this.stateSeq.get(dbAppKey(state)).add(() => this.atomicRunStateMachine(state, event, srcEvent));
+ return;
+ }
+ if (rDbToken.Ok().status === "not-bound") {
+ this.logger.Debug().Any({ appId: state.appId, dbName: state.dbName }).Msg("DB is not bound, requesting login");
+ // make all dbs go to waiting state
+ state.setState("waiting");
+ return this.requestPageToDoLogin(state, event, srcEvent);
+ } else {
+ 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(state)).add(() => this.atomicRunStateMachine(state, event, srcEvent));
+ return;
+ }
+ const res = rCloudToken.Ok().res;
+ if (!isResCloudDbTokenBound(res)) {
+ return this.requestPageToDoLogin(state, 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(state)).add(() => this.atomicRunStateMachine(state, event, srcEvent));
+ return;
+ }
+ const { token, claims } = rTandC.Ok();
+ const fpccEvtApp: FPCCEvtApp = {
+ tid: event.tid,
+ type: "FPCCEvtApp",
+ src: "iframe",
+ dst: event.src,
+ appId: state.appId,
+ appFavIcon: {
+ defURL: "https://fireproof.direct/favicon.ico",
+ },
+ devId: state.deviceId,
+ user: {
+ name: claims.nickname ?? claims.userId,
+ email: claims.email,
+ provider: claims.provider ?? "unknown",
+ iconURL: "https://fireproof.direct/favicon.ico",
+ },
+ localDb: {
+ dbName: state.dbName,
+ tenantId: claims.selected.tenant,
+ ledgerId: claims.selected.ledger,
+ accessToken: token,
+ },
+ env: {},
+ };
+ await this.backendFPCC.setFPCCEvtApp(fpccEvtApp);
+ state.setState("ready");
+ this.logger
+ .Debug()
+ .Any({ appId: state.appId, dbName: state.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.onFPCCReqRegisterLocalDbName(async (event, srcEvent: MessageEvent) => {
+ return this.runStateMachine(event, srcEvent);
+ });
+
+ return Result.Ok({
+ type: "ready"
+ });
+ });
+
+ 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/cloud/connector/test/package.json b/cloud/connector/test/package.json
new file mode 100644
index 000000000..6d8d99437
--- /dev/null
+++ b/cloud/connector/test/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@fireproof/cloud-connector-test",
+ "version": "0.0.0",
+ "description": "cloud connector shared test",
+ "type": "module",
+ "private": "true",
+ "scripts": {
+ "build": "core-cli tsc",
+ "pack": "echo skip",
+ "publish": "echo skip",
+ "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-svc": "workspace:*",
+ "@fireproof/cloud-connector-page": "workspace:*",
+ "@fireproof/core-runtime": "workspace:*",
+ "@vitest/browser": "^4.0.8",
+ "@vitest/browser-playwright": "^4.0.8",
+ "ts-essentials": "^10.1.1",
+ "vitest": "^4.0.8"
+ }
+}
diff --git a/cloud/connector/test/page-fpcc-protocol.test.ts b/cloud/connector/test/page-fpcc-protocol.test.ts
new file mode 100644
index 000000000..f9914fb4e
--- /dev/null
+++ b/cloud/connector/test/page-fpcc-protocol.test.ts
@@ -0,0 +1,120 @@
+import { describe, expect, it, vi } from "vitest";
+import { FPCloudFrontend, pageFPCCProtocol } from "@fireproof/cloud-connector-page";
+import { SvcFPCCProtocol } from "@fireproof/cloud-connector-svc";
+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 = pageFPCCProtocol({
+ sthis,
+ iframeHref: "https://example.com/iframe",
+ loginWaitTime: 1000,
+ fpCloudFrontend: new TestFrontend(),
+ });
+ const iframeProtocol = new SvcFPCCProtocol(sthis, {
+ dashboardURI: "https://example.com/dashboard",
+ cloudApiURI: "https://example.com/wait-for-token",
+ });
+
+ iframeProtocol.injectSend((evt: Writable) => {
+ evt.src = evt.src ?? "iframe";
+ // console.log("IframeFPCCProtocol sending message", evt);
+ pageProtocol.fpccProtocol.handleMessage({ data: evt, origin: "iframe" } 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", async () => {
+ const pingMessage: FPCCPing = {
+ tid: "test-ping-1",
+ type: "FPCCPing",
+ src: "page",
+ dst: "iframe",
+ timestamp: Date.now(),
+ };
+ const fpccFn = vi.fn();
+ pageProtocol.fpccProtocol.onFPCCMessage(fpccFn);
+ await protocolStart();
+ pageProtocol.fpccProtocol.sendMessage(pingMessage, "iframe");
+ 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 () => {
+ // 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/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/cloud/connector/test/vitest.config.ts b/cloud/connector/test/vitest.config.ts
new file mode 100644
index 000000000..1d1b73653
--- /dev/null
+++ b/cloud/connector/test/vitest.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from "vitest/config";
+import { playwright } from "@vitest/browser-playwright";
+
+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({
+ // ...custom playwright options
+ }),
+ instances: [
+ {
+ browser: "chromium",
+ },
+ ],
+ screenshotFailures: false,
+ },
+ },
+});
diff --git a/core/device-id/device-id-protocol.ts b/core/device-id/device-id-protocol.ts
index 3379e4cd5..7ee7edb8b 100644
--- a/core/device-id/device-id-protocol.ts
+++ b/core/device-id/device-id-protocol.ts
@@ -1,14 +1,15 @@
-import { IssueCertificateResult, JWKPrivateSchema, SuperThis } from "@fireproof/core-types-base";
-import { CAActions, DeviceIdCA } from "./device-id-CA.js";
+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, 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 +30,13 @@ async function ensureCA(sthis: SuperThis, actions: CAActions): Promise {
+ // const now = Date.now();
+ const hash = await hashObjectAsync(pub);
+ return hash;
+ },
+ },
}),
);
}
@@ -40,14 +47,19 @@ export interface DeviceIdProtocol {
}
export interface DeviceIdProtocolSrvOpts {
- readonly actions: CAActions;
+ // usally from ENV
+ readonly env?: {
+ readonly DEVICE_ID_CA_KEY: string;
+ readonly DEVICE_ID_CA_COMMON_NAME?: string;
+ };
+ // readonly actions: CAActions;
}
export class DeviceIdProtocolSrv implements DeviceIdProtocol {
readonly #ca: DeviceIdCA;
readonly #verifyMsg: DeviceIdVerifyMsg;
static async create(sthis: SuperThis, opts: DeviceIdProtocolSrvOpts): Promise> {
- const rCa = await ensureCA(sthis, opts.actions);
+ const rCa = await ensureCA(sthis, opts);
if (rCa.isErr()) {
return Result.Err(rCa);
}
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..56cf488e5 100644
--- a/core/gateways/cloud/to-cloud.ts
+++ b/core/gateways/cloud/to-cloud.ts
@@ -1,8 +1,8 @@
-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,
- FPCloudClaimParseSchema,
+ FPCloudClaimSchema,
FPCloudUri,
hashableFPCloudRef,
ToCloudAttachable,
@@ -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,12 +31,13 @@ function addTenantAndLedger(opts: ToCloudOptionalOpts, uri: CoerceURI): URI {
}
export class SimpleTokenStrategy implements TokenStrategie {
- private tc: TokenAndClaims;
+ readonly waitState: "started" | "stopped" = "stopped";
+ private tc: TokenAndSelectedTenantAndLedger;
constructor(jwk: string) {
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 {
@@ -70,13 +71,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 +122,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 +209,7 @@ class ToCloud implements ToCloudAttachable {
return this.opts.name;
}
- private _tokenObserver!: TokenObserver;
+ // private _tokenObserver!: TokenObserver;
configHash(db?: Ledger) {
const hash = hashObjectSync({
@@ -240,13 +241,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 +271,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/gateways/file-node/node-filesystem.ts b/core/gateways/file-node/node-filesystem.ts
index 411c213d8..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 {
@@ -45,38 +46,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/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/protocols/dashboard/msg-api.ts b/core/protocols/dashboard/msg-api.ts
index 1a11dec2f..f9d5b0682 100644
--- a/core/protocols/dashboard/msg-api.ts
+++ b/core/protocols/dashboard/msg-api.ts
@@ -1,14 +1,27 @@
-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";
+import { ensureLogger } from "@fireproof/core-runtime";
+import { SuperThis } from "@fireproof/core-types-base";
-export class Api {
+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;
}
+ readonly hash = Lazy(() => this.apiUrl);
+
async request(req: Q): Promise> {
return exception2Result(async () => {
const res = await fetch(this.apiUrl.toString(), {
@@ -27,6 +40,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..7894bf2cd 100644
--- a/core/protocols/dashboard/msg-is.ts
+++ b/core/protocols/dashboard/msg-is.ts
@@ -18,6 +18,9 @@ import {
ReqDeleteLedger,
ResTokenByResultId,
ReqExtendToken,
+ ReqClerkPublishableKey,
+ ResClerkPublishableKey,
+ ReqCloudDbToken,
} from "./msg-types.js";
interface FPApiMsgInterface {
@@ -32,9 +35,12 @@ 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;
+ isResClerkPublishableKey(jso: unknown): jso is ResClerkPublishableKey;
isListLedgersByUser(jso: unknown): jso is ReqListLedgersByUser;
isCreateLedger(jso: unknown): jso is ReqCreateLedger;
isUpdateLedger(jso: unknown): jso is ReqUpdateLedger;
@@ -91,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 {
@@ -103,4 +109,13 @@ 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");
+ }
+ 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 b97a2610b..dadf7b37c 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,21 @@ 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 defaultTenant?: boolean;
readonly ownerUserId: string;
- readonly maxAdminUsers?: number;
- readonly maxMemberUsers?: number;
- readonly maxInvites?: number;
-}
+} & Partial;
export interface ReqCreateTenant {
readonly type: "reqCreateTenant";
@@ -405,6 +414,48 @@ 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;
+ // helps in the binding process
+ readonly ledgers: {
+ readonly ledgerId: string;
+ readonly tenantId: string;
+ readonly name: 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 +473,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/runtime/utils.ts b/core/runtime/utils.ts
index 16595d7eb..1c7846202 100644
--- a/core/runtime/utils.ts
+++ b/core/runtime/utils.ts
@@ -71,6 +71,10 @@ 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 +634,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/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/tests/blockstore/standalone.test.ts b/core/tests/blockstore/standalone.test.ts
index e539478f1..405e9f217 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..b48242007 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..20cd6bd86 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/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/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/core/types/protocols/cloud/gateway-control.ts b/core/types/protocols/cloud/gateway-control.ts
index bc72880a7..ec6829dd9 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 } 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";
+// 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;
@@ -17,10 +23,17 @@ export interface TokenAndClaims {
}
export interface TokenStrategie {
+ waitState: "started" | "stopped";
+ ready?: () => Promise;
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;
}
@@ -53,7 +66,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/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/.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/api.ts b/dashboard/backend/api.ts
index 101d2a2aa..80d2c3d66 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";
@@ -53,6 +52,11 @@ import {
UserStatus,
VerifiedAuth,
FAPIMsgImpl,
+ FPApiParameters,
+ ResClerkPublishableKey,
+ ReqClerkPublishableKey,
+ ReqCloudDbToken,
+ ResCloudDbToken,
} from "@fireproof/core-protocols-dashboard";
import { prepareInviteTicket, sqlInviteTickets, sqlToInviteTickets } from "./invites.js";
import { sqlLedgerUsers, sqlLedgers, sqlToLedgers } from "./ledgers.js";
@@ -62,8 +66,10 @@ 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";
+import { getCloudDbToken, getCloudSessionToken } from "./cloud-token.js";
function sqlToOutTenantParams(sql: typeof sqlTenants.$inferSelect): OutTenantParams {
return {
@@ -115,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>;
}
@@ -162,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;
@@ -174,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;
@@ -200,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}`;
}
@@ -224,10 +242,19 @@ 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;
+ }
+
+ 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> {
@@ -249,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()) {
@@ -278,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(
@@ -313,16 +340,15 @@ 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),
tenantId: rTenant.Ok().tenantId,
userId: userId,
role: "admin",
- default: true,
+ defaultTenant: true,
});
// });
@@ -330,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,
@@ -371,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,
@@ -382,7 +408,7 @@ export class FPApiSQL implements FPApiInterface {
return Result.Err(rCheck.Err());
}
const now = new Date().toISOString();
- if (req.default) {
+ if (req.defaultTenant) {
await db
.update(sqlTenantUsers)
.set({
@@ -392,6 +418,7 @@ export class FPApiSQL implements FPApiInterface {
.where(and(eq(sqlTenantUsers.userId, req.userId), ne(sqlTenantUsers.default, 0)))
.run();
}
+ // console.log("Adding user to tenant:", req.userId, "->", req.tenantId, "as", req.defaultTenant);
const ret = (
await db
.insert(sqlTenantUsers)
@@ -400,18 +427,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),
@@ -486,7 +519,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),
@@ -498,7 +531,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({
@@ -517,7 +550,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,
})
@@ -531,7 +564,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),
});
@@ -543,7 +576,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
@@ -766,7 +799,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({
@@ -889,7 +922,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);
@@ -1039,7 +1072,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);
@@ -1166,7 +1199,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"));
@@ -1316,7 +1349,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);
@@ -1337,7 +1370,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;
@@ -1403,10 +1436,11 @@ 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, {
+ ...this.params,
...req.tenant,
ownerUserId: auth.user.userId,
});
@@ -1419,15 +1453,19 @@ 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,
});
}
- 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 +1478,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,
})
@@ -1456,7 +1495,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();
@@ -1490,7 +1529,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
@@ -1542,7 +1581,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
@@ -1602,7 +1641,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();
@@ -1694,7 +1733,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();
@@ -1715,7 +1754,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();
@@ -1736,142 +1775,11 @@ 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();
+ async getClerkPublishableKey(_req: ReqClerkPublishableKey): Promise> {
return Result.Ok({
- type: "resTokenByResultId",
- ...req,
+ type: "resClerkPublishableKey",
+ publishableKey: this.params.clerkPublishableKey,
+ cloudPublicKeys: this.params.cloudPublicKeys,
});
}
@@ -1942,71 +1850,3 @@ export class FPApiSQL implements FPApiInterface {
}
}
}
-
-function toProvider(i: ClerkVerifyAuth): FPCloudClaim["provider"] {
- if (i.params.nick) {
- return "github";
- }
- 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/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/cf-serve.ts b/dashboard/backend/cf-serve.ts
index e1ce7ec8e..95a1897b9 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,9 +26,26 @@ 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("/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.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"): {
+ console.log("direct request", request.method, uri.toString());
+ return env.ASSETS.fetch(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
new file mode 100644
index 000000000..dce63a31a
--- /dev/null
+++ b/dashboard/backend/cloud-token.ts
@@ -0,0 +1,446 @@
+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) {
+ // console.log("createBoundToken", auth);
+ 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-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..e0de5c4b0 100644
--- a/dashboard/backend/create-handler.ts
+++ b/dashboard/backend/create-handler.ts
@@ -2,13 +2,14 @@
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";
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