@fluid-app/droplet-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import { CallbackTokenStore, StoredRegistration } from "./types.mjs";
2
+
3
+ //#region src/store/drizzle.d.ts
4
+ /**
5
+ * Minimal surface of the Drizzle table and database this adapter needs.
6
+ *
7
+ * Typed structurally so the package does not depend on a specific Drizzle
8
+ * version or dialect, and so a Prisma-backed and a Drizzle-backed droplet can
9
+ * use the same store interface.
10
+ */
11
+ interface DrizzleCallbackTable {
12
+ uuid: unknown;
13
+ dri: unknown;
14
+ definitionName: unknown;
15
+ tokenDigest: unknown;
16
+ url: unknown;
17
+ }
18
+ interface DrizzleLike {
19
+ select(): {
20
+ from(table: DrizzleCallbackTable): {
21
+ where(condition: unknown): {
22
+ limit(n: number): Promise<StoredRegistration[]>;
23
+ };
24
+ };
25
+ };
26
+ insert(table: DrizzleCallbackTable): {
27
+ values(row: StoredRegistration): {
28
+ onConflictDoUpdate(args: {
29
+ target: unknown;
30
+ set: Omit<StoredRegistration, "uuid">;
31
+ }): Promise<unknown>;
32
+ };
33
+ };
34
+ delete(table: DrizzleCallbackTable): {
35
+ where(condition: unknown): Promise<unknown>;
36
+ };
37
+ }
38
+ /** `eq` from drizzle-orm, injected so the package need not depend on it directly. */
39
+ type EqFn = (column: unknown, value: unknown) => unknown;
40
+ /**
41
+ * Wraps a Drizzle table as a CallbackTokenStore.
42
+ *
43
+ * Usage:
44
+ *
45
+ * import { eq } from "drizzle-orm";
46
+ * createDrizzleCallbackStore(db, fluidCallbackRegistrations, eq)
47
+ */
48
+ declare function createDrizzleCallbackStore(db: DrizzleLike, table: DrizzleCallbackTable, eq: EqFn): CallbackTokenStore;
49
+ //#endregion
50
+ export { DrizzleCallbackTable, DrizzleLike, EqFn, createDrizzleCallbackStore };
@@ -0,0 +1,20 @@
1
+ //#region src/store/drizzle.ts
2
+ function createDrizzleCallbackStore(db, table, eq) {
3
+ return {
4
+ async findByTokenDigest(digest) {
5
+ return (await db.select().from(table).where(eq(table.tokenDigest, digest)).limit(1))[0] ?? null;
6
+ },
7
+ async upsert(registration) {
8
+ const { uuid, ...rest } = registration;
9
+ await db.insert(table).values(registration).onConflictDoUpdate({
10
+ target: table.uuid,
11
+ set: rest
12
+ });
13
+ },
14
+ async deleteForInstallation(dri) {
15
+ await db.delete(table).where(eq(table.dri, dri));
16
+ }
17
+ };
18
+ }
19
+ //#endregion
20
+ export { createDrizzleCallbackStore };
@@ -0,0 +1,46 @@
1
+ import { CallbackTokenStore, StoredRegistration } from "./types.mjs";
2
+
3
+ //#region src/store/prisma.d.ts
4
+ /**
5
+ * The delegate shape this adapter needs.
6
+ *
7
+ * Structural rather than importing a generated Prisma client: each droplet
8
+ * names the model differently, and the package must not depend on any one
9
+ * droplet's generated types.
10
+ */
11
+ interface PrismaCallbackDelegate {
12
+ findUnique(args: {
13
+ where: {
14
+ tokenDigest: string;
15
+ };
16
+ }): Promise<StoredRegistration | null>;
17
+ upsert(args: {
18
+ where: {
19
+ uuid: string;
20
+ };
21
+ create: StoredRegistration;
22
+ update: Omit<StoredRegistration, "uuid">;
23
+ }): Promise<unknown>;
24
+ deleteMany(args: {
25
+ where: {
26
+ dri: string;
27
+ };
28
+ }): Promise<unknown>;
29
+ count(args: {
30
+ where: {
31
+ definitionName: {
32
+ in: string[];
33
+ };
34
+ };
35
+ }): Promise<number>;
36
+ }
37
+ /**
38
+ * Wraps a Prisma model delegate as a CallbackTokenStore.
39
+ *
40
+ * Usage:
41
+ *
42
+ * createPrismaCallbackStore(prisma.fluidCallbackRegistration)
43
+ */
44
+ declare function createPrismaCallbackStore(delegate: PrismaCallbackDelegate): CallbackTokenStore;
45
+ //#endregion
46
+ export { PrismaCallbackDelegate, createPrismaCallbackStore };
@@ -0,0 +1,21 @@
1
+ //#region src/store/prisma.ts
2
+ function createPrismaCallbackStore(delegate) {
3
+ return {
4
+ async findByTokenDigest(digest) {
5
+ return delegate.findUnique({ where: { tokenDigest: digest } });
6
+ },
7
+ async upsert(registration) {
8
+ const { uuid, ...rest } = registration;
9
+ await delegate.upsert({
10
+ where: { uuid },
11
+ create: registration,
12
+ update: rest
13
+ });
14
+ },
15
+ async deleteForInstallation(dri) {
16
+ await delegate.deleteMany({ where: { dri } });
17
+ }
18
+ };
19
+ }
20
+ //#endregion
21
+ export { createPrismaCallbackStore };
@@ -0,0 +1,26 @@
1
+ //#region src/store/types.d.ts
2
+ /**
3
+ * Storage port for callback verification tokens.
4
+ *
5
+ * Deliberately narrow: this package owns one table and nothing else.
6
+ * Installation and company storage stay app-owned.
7
+ */
8
+ interface StoredRegistration {
9
+ /** Fluid's `cbr_` registration uuid. */
10
+ uuid: string;
11
+ /** droplet_installation_uuid (`dri_`) this registration belongs to. */
12
+ dri: string;
13
+ /** Which callback definition this registration serves. */
14
+ definitionName: string;
15
+ /** sha256 of the `cvt_` token. Never the token itself. */
16
+ tokenDigest: string;
17
+ url: string;
18
+ }
19
+ interface CallbackTokenStore {
20
+ /** Locate a registration by the digest of a presented token. Must be an indexed lookup. */
21
+ findByTokenDigest(digest: string): Promise<StoredRegistration | null>;
22
+ upsert(registration: StoredRegistration): Promise<void>;
23
+ deleteForInstallation(dri: string): Promise<void>;
24
+ }
25
+ //#endregion
26
+ export { CallbackTokenStore, StoredRegistration };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@fluid-app/droplet-sdk",
3
+ "version": "0.2.0",
4
+ "description": "Signature verification and tenancy for Fluid droplet callbacks and webhooks",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fluid-commerce/fluid.git",
10
+ "directory": "packages/platform/droplet-sdk"
11
+ },
12
+ "publishConfig": {
13
+ "registry": "https://registry.npmjs.org",
14
+ "access": "public"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "schema"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.mts",
23
+ "default": "./dist/index.mjs"
24
+ },
25
+ "./next": {
26
+ "types": "./dist/next.d.mts",
27
+ "default": "./dist/next.mjs"
28
+ },
29
+ "./prisma": {
30
+ "types": "./dist/prisma.d.mts",
31
+ "default": "./dist/prisma.mjs"
32
+ },
33
+ "./drizzle": {
34
+ "types": "./dist/drizzle.d.mts",
35
+ "default": "./dist/drizzle.mjs"
36
+ },
37
+ "./schema/callback-registrations.prisma": "./schema/callback-registrations.prisma",
38
+ "./checkout": {
39
+ "types": "./dist/checkout.d.mts",
40
+ "default": "./dist/checkout.mjs"
41
+ }
42
+ },
43
+ "peerDependencies": {
44
+ "typescript": "^5"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "typescript": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "dependencies": {
52
+ "zod": "4.3.5"
53
+ },
54
+ "devDependencies": {
55
+ "@electric-sql/pglite": "^0.5.5",
56
+ "@fluid-app/typescript-config": "0.0.0",
57
+ "@types/node": "24.10.12",
58
+ "drizzle-orm": "^0.45.2",
59
+ "tsdown": "^0.21.0",
60
+ "typescript": "^5",
61
+ "vitest": "^4.0.18"
62
+ },
63
+ "scripts": {
64
+ "build": "tsdown",
65
+ "dev": "tsdown --watch",
66
+ "lint": "oxlint",
67
+ "lint:fix": "oxlint --fix",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "typecheck": "tsgo --noEmit",
71
+ "vectors:generate": "ruby vectors/generate.rb ../../../apps/rails > vectors/fluid-signing.json",
72
+ "vectors:check": "ruby vectors/generate.rb ../../../apps/rails | diff -u vectors/fluid-signing.json -",
73
+ "check:publish-shape": "node scripts/check-publish-shape.mjs"
74
+ }
75
+ }
@@ -0,0 +1,21 @@
1
+ // Callback verification tokens, owned by @fluid-app/droplet-sdk.
2
+ //
3
+ // Paste into a droplet's schema.prisma. The shape is deliberately fixed, so it
4
+ // stays identical wherever it is used and does not depend on the ORM.
5
+ //
6
+ // Only the digest is stored: Fluid presents the plaintext token on every
7
+ // request, so a digest is enough to locate the registration, and a database
8
+ // dump then yields no working callback credentials.
9
+ model FluidCallbackRegistration {
10
+ uuid String @id
11
+ dri String
12
+ definitionName String @map("definition_name")
13
+ tokenDigest String @unique @map("token_digest")
14
+ url String
15
+
16
+ createdAt DateTime @default(now()) @map("created_at")
17
+ updatedAt DateTime @updatedAt @map("updated_at")
18
+
19
+ @@index([dri])
20
+ @@map("fluid_callback_registrations")
21
+ }