Skip to content

Commit 3ce9629

Browse files
mabelsclaude
andcommitted
refactor(dashboard): promote FP cloud token utils to core-protocols-dashboard
Move `createFPToken`, `getFPTokenContext`, `nameFromAuth`, `nickFromClarkClaim`, and `toProvider` from `dashboard/backend/utils/` into the shared `core/protocols/dashboard/fp-cloud-token.ts`. Also move `FPTokenContext` interface from `dashboard/backend/types.ts` into `core/types/protocols/dashboard/`. Dashboard backend now imports these from `@fireproof/core-protocols-dashboard` so they can be reused outside the backend package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aa68cb6 commit 3ce9629

File tree

10 files changed

+104
-93
lines changed

10 files changed

+104
-93
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { FPCloudClaim } from "@fireproof/core-types-protocols-cloud";
2+
import { param, Result } from "@adviser/cement";
3+
import { ClerkClaim, ClerkEmailTemplateClaim, SuperThis } from "@fireproof/core-types-base";
4+
import { sts } from "@fireproof/core-runtime";
5+
import { SignJWT } from "jose/jwt/sign";
6+
import { FPTokenContext, VerifiedAuthUserResult } from "@fireproof/core-types-protocols-dashboard";
7+
8+
export async function createFPToken(ctx: FPTokenContext, claim: FPCloudClaim) {
9+
const privKeys = await sts.env2jwk(ctx.secretToken);
10+
if (privKeys.length !== 1) {
11+
throw new Error(`Expected exactly one private JWK, found ${privKeys.length}`);
12+
}
13+
const privKey = privKeys[0];
14+
let validFor = ctx.validFor;
15+
if (validFor <= 0) {
16+
validFor = 60 * 60; // 1 hour
17+
}
18+
const expiresDate = new Date(Date.now() + validFor * 1000); // epoch sec
19+
const expiresInSec = Math.floor((Math.floor(expiresDate.getTime() / 1000) + validFor) / 1000);
20+
const epochExp = Math.floor(expiresDate.getTime() / 1000);
21+
return {
22+
expiresDate,
23+
expiresInSec,
24+
token: await new SignJWT(claim)
25+
.setProtectedHeader({ alg: "ES256" }) // algorithm
26+
.setIssuedAt()
27+
.setIssuer(ctx.issuer) // issuer
28+
.setAudience(ctx.audience) // audience
29+
.setExpirationTime(epochExp) // expiration time
30+
.sign(privKey),
31+
};
32+
}
33+
34+
export async function getFPTokenContext(sthis: SuperThis, ictx: Partial<FPTokenContext> = {}): Promise<Result<FPTokenContext>> {
35+
const rCtx = sthis.env.gets({
36+
CLOUD_SESSION_TOKEN_SECRET: ictx.secretToken ?? param.REQUIRED,
37+
CLOUD_SESSION_TOKEN_PUBLIC: ictx.publicToken ?? param.REQUIRED,
38+
CLOUD_SESSION_TOKEN_ISSUER: "FP_CLOUD",
39+
CLOUD_SESSION_TOKEN_AUDIENCE: "PUBLIC",
40+
CLOUD_SESSION_TOKEN_VALID_FOR: "" + 60 * 60,
41+
CLOUD_SESSION_TOKEN_EXTEND_VALID_FOR: "" + 6 * 60 * 60,
42+
});
43+
if (rCtx.isErr()) {
44+
return Result.Err(rCtx.Err());
45+
}
46+
const ctx = rCtx.Ok();
47+
return Result.Ok({
48+
secretToken: ctx.CLOUD_SESSION_TOKEN_SECRET,
49+
publicToken: ctx.CLOUD_SESSION_TOKEN_PUBLIC,
50+
issuer: ctx.CLOUD_SESSION_TOKEN_ISSUER,
51+
audience: ctx.CLOUD_SESSION_TOKEN_AUDIENCE,
52+
validFor: parseInt(ctx.CLOUD_SESSION_TOKEN_VALID_FOR, 10),
53+
extendValidFor: parseInt(ctx.CLOUD_SESSION_TOKEN_EXTEND_VALID_FOR, 10),
54+
...ictx,
55+
} satisfies FPTokenContext);
56+
}
57+
58+
export function nameFromAuth(name: string | undefined, auth: VerifiedAuthUserResult): string {
59+
return (
60+
name ?? `${auth.verifiedAuth.claims.params.email ?? nickFromClarkClaim(auth.verifiedAuth.claims.params) ?? auth.user.userId}`
61+
);
62+
}
63+
64+
export function nickFromClarkClaim(auth: ClerkEmailTemplateClaim): string | undefined {
65+
return auth.nick ?? auth.name ?? undefined;
66+
}
67+
68+
export function toProvider(i: ClerkClaim): FPCloudClaim["provider"] {
69+
if (i.params.nick) {
70+
return "github";
71+
}
72+
return "google";
73+
}

core/protocols/dashboard/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from "./msg-api.js";
22
export * from "./dashboard-api.js";
33
export * from "./token.js";
44
export * from "./get-cloud-pubkey-from-env.js";
5+
6+
export * from "./fp-cloud-token.js";

core/types/protocols/dashboard/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ export * from "./msg-is.js";
44
export * from "./msg-types.js";
55

66
export * from "./token.js";
7+
export interface FPTokenContext {
8+
readonly secretToken: string;
9+
readonly publicToken: string;
10+
readonly issuer: string;
11+
readonly audience: string;
12+
readonly validFor: number; // seconds
13+
readonly extendValidFor: number; // seconds
14+
}

dashboard/actions/base/action.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ runs:
2929
working-directory: dashboard/frontend
3030
env:
3131
VITE_CLERK_PUBLISHABLE_KEY: ${{ inputs.CLERK_PUBLISHABLE_KEY }}
32-
run: pnpm run build
32+
run: FP_TSC=tsc pnpm run build
3333

3434
- name: FRONTEND test
3535
shell: bash
@@ -57,7 +57,7 @@ runs:
5757
working-directory: dashboard/backend
5858
env:
5959
VITE_CLERK_PUBLISHABLE_KEY: ${{ inputs.CLERK_PUBLISHABLE_KEY }}
60-
run: pnpm run build
60+
run: FP_TSC=tsc pnpm run build
6161

6262
- name: BACKEND test
6363
shell: bash

dashboard/backend/public/create-tenant.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { EventoHandler, Result, EventoResultType, HandleTriggerCtx } from "@adviser/cement";
22
import { ReqCreateTenant, ResCreateTenant, validateCreateTenant } from "@fireproof/core-types-protocols-dashboard";
33
import { FPApiSQLCtx, ReqWithVerifiedAuthUser } from "../types.js";
4-
import { nameFromAuth, checkAuth, wrapStop } from "../utils/index.js";
4+
import { checkAuth, wrapStop } from "../utils/index.js";
55
import { insertTenant } from "../internal/insert-tenant.js";
66
import { addUserToTenant } from "../internal/add-user-to-tenant.js";
7+
import { nameFromAuth } from "@fireproof/core-protocols-dashboard";
78

89
async function createTenant(ctx: FPApiSQLCtx, req: ReqWithVerifiedAuthUser<ReqCreateTenant>): Promise<Result<ResCreateTenant>> {
910
const rTenant = await insertTenant(ctx, req.auth.user, {

dashboard/backend/public/ensure-cloud-token.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EventoHandler, Result, EventoResultType, HandleTriggerCtx } from "@adviser/cement";
22
import {
3+
FPTokenContext,
34
ReqEnsureCloudToken,
45
ResEnsureCloudToken,
56
validateEnsureCloudToken,
@@ -9,12 +10,13 @@ import { FPCloudClaimSchema } from "@fireproof/core-types-protocols-cloud";
910
import { eq, and, count } from "drizzle-orm";
1011
import { sqlAppIdBinding } from "../sql/app-id-bind.js";
1112
import { sqlLedgers, sqlLedgerUsers } from "../sql/ledgers.js";
12-
import { FPApiSQLCtx, FPTokenContext, ReqWithVerifiedAuthUser } from "../types.js";
13-
import { getFPTokenContext, createFPToken, toProvider, checkAuth, wrapStop } from "../utils/index.js";
13+
import { FPApiSQLCtx, ReqWithVerifiedAuthUser } from "../types.js";
14+
import { checkAuth, wrapStop } from "../utils/index.js";
1415
import { createLedger } from "./create-ledger.js";
1516
import { ensureUser } from "./ensure-user.js";
1617
import { listLedgersByUser } from "./list-ledgers-by-user.js";
1718
import { decodeJwt } from "jose";
19+
import { getFPTokenContext, createFPToken, toProvider } from "@fireproof/core-protocols-dashboard";
1820

1921
function getAppIdBinding(
2022
ctx: FPApiSQLCtx,

dashboard/backend/public/ensure-user.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import { getTableColumns, eq } from "drizzle-orm";
1111
import { queryEmail, queryNick } from "../sql/sql-helper.js";
1212
import { sqlTenants } from "../sql/tenants.js";
1313
import { upsetUserByProvider, sqlUsers, UserByProviderWithoutDate } from "../sql/users.js";
14-
import { nickFromClarkClaim, nameFromAuth, wrapStop, verifyAuth } from "../utils/index.js";
14+
import { wrapStop, verifyAuth } from "../utils/index.js";
1515
import { addUserToTenant } from "../internal/add-user-to-tenant.js";
1616
import { insertTenant } from "../internal/insert-tenant.js";
1717
import { FPApiSQLCtx } from "../types.js";
1818
import { listTenantsByUser } from "./list-tenants-by-user.js";
1919
import { redeemInvite } from "./redeem-invite.js";
20+
import { nickFromClarkClaim, nameFromAuth } from "@fireproof/core-protocols-dashboard";
2021

2122
export async function ensureUser(ctx: FPApiSQLCtx, req: ReqEnsureUser): Promise<Result<ResEnsureUser>> {
2223
const rAuth = await verifyAuth(ctx, req);

dashboard/backend/public/get-cloud-session-token.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { EventoHandler, Result, EventoResultType, HandleTriggerCtx } from "@adviser/cement";
2-
import { ReqCloudSessionToken, ResCloudSessionToken, validateCloudSessionToken } from "@fireproof/core-types-protocols-dashboard";
2+
import {
3+
FPTokenContext,
4+
ReqCloudSessionToken,
5+
ResCloudSessionToken,
6+
validateCloudSessionToken,
7+
} from "@fireproof/core-types-protocols-dashboard";
38
import { FPCloudClaim } from "@fireproof/core-types-protocols-cloud";
4-
import { getFPTokenContext, createFPToken, toProvider, checkAuth, wrapStop } from "../utils/index.js";
5-
import { FPApiSQLCtx, FPTokenContext, ReqWithVerifiedAuthUser } from "../types.js";
9+
import { checkAuth, wrapStop } from "../utils/index.js";
10+
import { FPApiSQLCtx, ReqWithVerifiedAuthUser } from "../types.js";
611
import { listTenantsByUser } from "./list-tenants-by-user.js";
712
import { listLedgersByUser } from "./list-ledgers-by-user.js";
813
import { addTokenByResultId } from "../internal/add-token-by-result-id.js";
14+
import { getFPTokenContext, createFPToken, toProvider } from "@fireproof/core-protocols-dashboard";
915

1016
async function getCloudSessionToken(
1117
ctx: FPApiSQLCtx,

dashboard/backend/types.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,6 @@ export type ReqWithVerifiedAuthUser<REQ extends { type: string; auth: DashAuthTy
3939
readonly auth: VerifiedAuthUserResult;
4040
};
4141

42-
export interface FPTokenContext {
43-
readonly secretToken: string;
44-
readonly publicToken: string;
45-
readonly issuer: string;
46-
readonly audience: string;
47-
readonly validFor: number; // seconds
48-
readonly extendValidFor: number; // seconds
49-
}
50-
5142
export interface FPApiSQLCtx {
5243
readonly db: DashSqlite;
5344
readonly logger: Logger;

dashboard/backend/utils/index.ts

Lines changed: 2 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,6 @@
1-
import { FPCloudClaim } from "@fireproof/core-types-protocols-cloud";
2-
import { EventoResult, EventoResultType, param, Result } from "@adviser/cement";
3-
import { ClerkClaim, ClerkEmailTemplateClaim, SuperThis } from "@fireproof/core";
4-
import { sts } from "@fireproof/core-runtime";
5-
import { SignJWT } from "jose/jwt/sign";
6-
import { FPTokenContext } from "../types.js";
7-
import { VerifiedAuthUserResult } from "@fireproof/core-types-protocols-dashboard";
1+
import { Result, EventoResultType, EventoResult } from "@adviser/cement";
82

9-
export async function createFPToken(ctx: FPTokenContext, claim: FPCloudClaim) {
10-
const privKeys = await sts.env2jwk(ctx.secretToken);
11-
if (privKeys.length !== 1) {
12-
throw new Error(`Expected exactly one private JWK, found ${privKeys.length}`);
13-
}
14-
const privKey = privKeys[0];
15-
let validFor = ctx.validFor;
16-
if (validFor <= 0) {
17-
validFor = 60 * 60; // 1 hour
18-
}
19-
const expiresDate = new Date(Date.now() + validFor * 1000); // epoch sec
20-
const expiresInSec = Math.floor((Math.floor(expiresDate.getTime() / 1000) + validFor) / 1000);
21-
const epochExp = Math.floor(expiresDate.getTime() / 1000);
22-
return {
23-
expiresDate,
24-
expiresInSec,
25-
token: await new SignJWT(claim)
26-
.setProtectedHeader({ alg: "ES256" }) // algorithm
27-
.setIssuedAt()
28-
.setIssuer(ctx.issuer) // issuer
29-
.setAudience(ctx.audience) // audience
30-
.setExpirationTime(epochExp) // expiration time
31-
.sign(privKey),
32-
};
33-
}
34-
35-
export async function getFPTokenContext(sthis: SuperThis, ictx: Partial<FPTokenContext> = {}): Promise<Result<FPTokenContext>> {
36-
const rCtx = sthis.env.gets({
37-
CLOUD_SESSION_TOKEN_SECRET: ictx.secretToken ?? param.REQUIRED,
38-
CLOUD_SESSION_TOKEN_PUBLIC: ictx.publicToken ?? param.REQUIRED,
39-
CLOUD_SESSION_TOKEN_ISSUER: "FP_CLOUD",
40-
CLOUD_SESSION_TOKEN_AUDIENCE: "PUBLIC",
41-
CLOUD_SESSION_TOKEN_VALID_FOR: "" + 60 * 60,
42-
CLOUD_SESSION_TOKEN_EXTEND_VALID_FOR: "" + 6 * 60 * 60,
43-
});
44-
if (rCtx.isErr()) {
45-
return Result.Err(rCtx.Err());
46-
}
47-
const ctx = rCtx.Ok();
48-
return Result.Ok({
49-
secretToken: ctx.CLOUD_SESSION_TOKEN_SECRET,
50-
publicToken: ctx.CLOUD_SESSION_TOKEN_PUBLIC,
51-
issuer: ctx.CLOUD_SESSION_TOKEN_ISSUER,
52-
audience: ctx.CLOUD_SESSION_TOKEN_AUDIENCE,
53-
validFor: parseInt(ctx.CLOUD_SESSION_TOKEN_VALID_FOR, 10),
54-
extendValidFor: parseInt(ctx.CLOUD_SESSION_TOKEN_EXTEND_VALID_FOR, 10),
55-
...ictx,
56-
} satisfies FPTokenContext);
57-
}
58-
59-
export function nameFromAuth(name: string | undefined, auth: VerifiedAuthUserResult): string {
60-
return (
61-
name ?? `${auth.verifiedAuth.claims.params.email ?? nickFromClarkClaim(auth.verifiedAuth.claims.params) ?? auth.user.userId}`
62-
);
63-
}
64-
65-
export function nickFromClarkClaim(auth: ClerkEmailTemplateClaim): string | undefined {
66-
return auth.nick ?? auth.name ?? undefined;
67-
}
68-
69-
export function toProvider(i: ClerkClaim): FPCloudClaim["provider"] {
70-
if (i.params.nick) {
71-
return "github";
72-
}
73-
return "google";
74-
}
3+
export * from "./auth.js";
754

765
export function wrapStop<T>(res: Promise<Result<T>>): Promise<Result<EventoResultType>> {
776
return res.then((r) => {
@@ -81,5 +10,3 @@ export function wrapStop<T>(res: Promise<Result<T>>): Promise<Result<EventoResul
8110
return Result.Ok(EventoResult.Stop);
8211
});
8312
}
84-
85-
export * from "./auth.js";

0 commit comments

Comments
 (0)