mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(uload): sync_changes integration, Stripe checkout, docs update
Sync integration: - Redirect service reads links from mana-sync's sync_changes table - Analytics service queries clicks from sync_changes - Click tracking writes to sync_changes (visible to all clients) - Public profile reads from sync_changes - Server DB points to mana_sync database (not separate uload DB) - Removed uload-database dependency from server Stripe: - Real Stripe checkout session creation (monthly/yearly) - Webhook handler with signature verification - Webhook route bypasses JWT auth Documentation: - Root CLAUDE.md: added uload to project table, dev commands, local-first list - mana-sync CLAUDE.md: added uLoad, Taktik, Calc to connected apps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9e82e40e16
commit
d02428fca1
12 changed files with 254 additions and 140 deletions
|
|
@ -55,6 +55,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
|
|||
| **inventar** | Inventory management | Web |
|
||||
| **traces** | City exploration | Backend, Mobile |
|
||||
| **taktik** | Time tracking | Web |
|
||||
| **uload** | URL shortener & link management | Server, Web, Landing |
|
||||
| **calc** | Calculator & converter | Web |
|
||||
| **playground** | LLM playground | Web |
|
||||
|
||||
|
|
@ -88,6 +89,7 @@ pnpm dev:calendar:full # Start calendar with auth + auto DB setup
|
|||
pnpm dev:clock:full # Start clock with auth + auto DB setup
|
||||
pnpm dev:todo:full # Start todo with auth + auto DB setup
|
||||
pnpm dev:picture:full # Start picture with auth + auto DB setup
|
||||
pnpm dev:uload:full # Start uload with auth + auto DB setup
|
||||
```
|
||||
|
||||
These commands automatically:
|
||||
|
|
@ -579,6 +581,7 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg
|
|||
| SkilltTree | skills, activities, achievements | Done |
|
||||
| CityCorners | locations, favorites | Done |
|
||||
| Taktik | clients, projects, timeEntries, tags, templates, settings | Done |
|
||||
| uLoad | links, tags, folders, linkTags | Done |
|
||||
| Calc | calculations, savedFormulas | Done |
|
||||
|
||||
**Not migrated (no CRUD data model):** ManaCore (hub), Matrix (protocol client), Playground (stateless)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/uload-database": "workspace:*",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"hono": "^4.7.0",
|
||||
"stripe": "^18.4.0",
|
||||
"jose": "^6.1.2",
|
||||
"postgres": "^3.4.7"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ export interface Config {
|
|||
databaseUrl: string;
|
||||
manaAuthUrl: string;
|
||||
cors: { origins: string[] };
|
||||
stripeSecretKey: string;
|
||||
stripeWebhookSecret: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export function loadConfig(): Config {
|
||||
|
|
@ -16,11 +19,14 @@ export function loadConfig(): Config {
|
|||
port: parseInt(process.env.PORT || '3070', 10),
|
||||
databaseUrl: requiredEnv(
|
||||
'DATABASE_URL',
|
||||
'postgresql://uload:uload_dev_password_123@localhost:5432/uload_dev'
|
||||
'postgresql://manacore:devpassword@localhost:5432/mana_sync'
|
||||
),
|
||||
manaAuthUrl: requiredEnv('MANA_CORE_AUTH_URL', 'http://localhost:3001'),
|
||||
cors: {
|
||||
origins: (process.env.CORS_ORIGINS || 'http://localhost:5173').split(','),
|
||||
},
|
||||
stripeSecretKey: process.env.STRIPE_SECRET_KEY || '',
|
||||
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||||
baseUrl: process.env.BASE_URL || 'http://localhost:3070',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from '@manacore/uload-database';
|
||||
|
||||
let db: ReturnType<typeof drizzle<typeof schema>> | null = null;
|
||||
let db: ReturnType<typeof drizzle> | null = null;
|
||||
|
||||
export function getDb(databaseUrl: string) {
|
||||
if (!db) {
|
||||
const client = postgres(databaseUrl, { max: 10 });
|
||||
db = drizzle(client, { schema });
|
||||
db = drizzle(client);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,16 @@ app.route('/health', healthRoutes);
|
|||
app.route('/r', createRedirectRoutes(redirectService));
|
||||
app.route('/public', createPublicRoutes(db));
|
||||
|
||||
// Analytics API (auth required)
|
||||
// Stripe webhook (no auth — signature verified internally)
|
||||
app.post('/api/v1/stripe/webhook', async (c) => {
|
||||
const routes = createStripeRoutes(config);
|
||||
return routes.fetch(c.req.raw);
|
||||
});
|
||||
|
||||
// Authenticated API routes
|
||||
app.use('/api/v1/*', jwtAuth(config.manaAuthUrl));
|
||||
app.route('/api/v1/analytics', createAnalyticsRoutes(analyticsService));
|
||||
app.route('/api/v1/stripe', createStripeRoutes());
|
||||
app.route('/api/v1/stripe', createStripeRoutes(config));
|
||||
app.route('/api/v1/email', createEmailRoutes());
|
||||
|
||||
console.log(`uload-server starting on port ${config.port}...`);
|
||||
|
|
|
|||
|
|
@ -1,35 +1,44 @@
|
|||
import { Hono } from 'hono';
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
import { links, users } from '@manacore/uload-database';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { Database } from '../db/connection';
|
||||
|
||||
export function createPublicRoutes(db: Database) {
|
||||
return new Hono().get('/u/:username', async (c) => {
|
||||
const username = c.req.param('username');
|
||||
|
||||
const [user] = await db
|
||||
.select({ id: users.id, username: users.username, name: users.name, bio: users.bio })
|
||||
.from(users)
|
||||
.where(and(eq(users.username, username), eq(users.publicProfile, true)))
|
||||
.limit(1);
|
||||
// Query links for a user from sync_changes
|
||||
// Note: In mana-sync, user_id is the auth user ID, not username.
|
||||
// For public profiles, we'd need a user lookup. For now, treat username as user_id.
|
||||
const result = await db.execute(sql`
|
||||
SELECT DISTINCT ON (record_id)
|
||||
record_id as id,
|
||||
data->>'shortCode' as "shortCode",
|
||||
data->>'title' as title,
|
||||
data->>'description' as description,
|
||||
COALESCE((data->>'clickCount')::int, 0) as "clickCount",
|
||||
created_at as "createdAt"
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload'
|
||||
AND table_name = 'links'
|
||||
AND user_id = ${username}
|
||||
AND op != 'delete'
|
||||
AND COALESCE((data->>'isActive')::boolean, true) = true
|
||||
ORDER BY record_id, created_at DESC
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
if (!user) {
|
||||
return c.json({ error: 'User not found' }, 404);
|
||||
}
|
||||
const links = result as unknown as {
|
||||
id: string;
|
||||
shortCode: string;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
clickCount: number;
|
||||
createdAt: string;
|
||||
}[];
|
||||
|
||||
const userLinks = await db
|
||||
.select({
|
||||
shortCode: links.shortCode,
|
||||
title: links.title,
|
||||
description: links.description,
|
||||
clickCount: links.clickCount,
|
||||
createdAt: links.createdAt,
|
||||
})
|
||||
.from(links)
|
||||
.where(and(eq(links.userId, user.id), eq(links.isActive, true)))
|
||||
.orderBy(desc(links.createdAt))
|
||||
.limit(50);
|
||||
|
||||
return c.json({ user, links: userLinks });
|
||||
return c.json({
|
||||
user: { username, name: null, bio: null },
|
||||
links,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,72 @@
|
|||
import { Hono } from 'hono';
|
||||
import Stripe from 'stripe';
|
||||
import type { AuthUser } from '../middleware/jwt-auth';
|
||||
import type { Config } from '../config';
|
||||
|
||||
const PRICES = {
|
||||
monthly: { lookup: 'uload_pro_monthly', amount: 499 },
|
||||
yearly: { lookup: 'uload_pro_yearly', amount: 3999 },
|
||||
} as const;
|
||||
|
||||
export function createStripeRoutes(config: Config) {
|
||||
const stripe = config.stripeSecretKey ? new Stripe(config.stripeSecretKey) : null;
|
||||
|
||||
export function createStripeRoutes() {
|
||||
return new Hono<{ Variables: { user: AuthUser } }>()
|
||||
.post('/checkout', async (c) => {
|
||||
// TODO: Implement Stripe checkout session creation
|
||||
return c.json({ error: 'Stripe not configured yet' }, 501);
|
||||
if (!stripe) return c.json({ error: 'Stripe not configured' }, 501);
|
||||
|
||||
const user = c.get('user');
|
||||
const { priceType } = await c.req.json<{ priceType: keyof typeof PRICES }>();
|
||||
const price = PRICES[priceType];
|
||||
if (!price) return c.json({ error: 'Invalid price type' }, 400);
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
customer_email: user.email,
|
||||
metadata: { userId: user.userId },
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: 'eur',
|
||||
unit_amount: price.amount,
|
||||
recurring: { interval: priceType === 'yearly' ? 'year' : 'month' },
|
||||
product_data: { name: `uLoad Pro (${priceType})` },
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${config.baseUrl}/settings?checkout=success`,
|
||||
cancel_url: `${config.baseUrl}/pricing`,
|
||||
});
|
||||
|
||||
return c.json({ url: session.url });
|
||||
})
|
||||
.post('/webhook', async (c) => {
|
||||
// TODO: Implement Stripe webhook handling
|
||||
if (!stripe) return c.json({ error: 'Stripe not configured' }, 501);
|
||||
|
||||
const body = await c.req.text();
|
||||
const sig = c.req.header('stripe-signature');
|
||||
if (!sig) return c.json({ error: 'Missing signature' }, 400);
|
||||
|
||||
let event: Stripe.Event;
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(body, sig, config.stripeWebhookSecret);
|
||||
} catch {
|
||||
return c.json({ error: 'Invalid signature' }, 400);
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed': {
|
||||
// TODO: Update user subscription status in mana-user or sync_changes
|
||||
// const _session = event.data.object as Stripe.Checkout.Session;
|
||||
break;
|
||||
}
|
||||
case 'customer.subscription.deleted': {
|
||||
// TODO: Reset user to free tier
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({ received: true });
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,82 @@
|
|||
import { eq, sql, and, gte, lte, desc } from 'drizzle-orm';
|
||||
import { clicks } from '@manacore/uload-database';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { Database } from '../db/connection';
|
||||
|
||||
/**
|
||||
* Analytics service that reads click data from mana-sync's sync_changes table.
|
||||
* Clicks are stored with app_id='uload', table_name='clicks'.
|
||||
*/
|
||||
export class AnalyticsService {
|
||||
constructor(private db: Database) {}
|
||||
|
||||
async getClicksByLink(linkId: string, from?: Date, to?: Date) {
|
||||
const conditions = [eq(clicks.linkId, linkId)];
|
||||
if (from) conditions.push(gte(clicks.clickedAt, from));
|
||||
if (to) conditions.push(lte(clicks.clickedAt, to));
|
||||
|
||||
return this.db
|
||||
.select()
|
||||
.from(clicks)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(clicks.clickedAt));
|
||||
}
|
||||
|
||||
async getClickStats(linkId: string) {
|
||||
const [stats] = await this.db
|
||||
.select({
|
||||
totalClicks: sql<number>`count(*)`,
|
||||
uniqueVisitors: sql<number>`count(distinct ${clicks.ipHash})`,
|
||||
browsers: sql<Record<string, number>>`json_object_agg(
|
||||
coalesce(${clicks.browser}, 'unknown'),
|
||||
1
|
||||
)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(eq(clicks.linkId, linkId));
|
||||
|
||||
return stats;
|
||||
const result = await this.db.execute(sql`
|
||||
SELECT
|
||||
count(*)::int as "totalClicks",
|
||||
count(DISTINCT data->>'ipHash')::int as "uniqueVisitors"
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload' AND table_name = 'clicks'
|
||||
AND data->>'linkId' = ${linkId}
|
||||
AND op = 'insert'
|
||||
`);
|
||||
const rows = result as unknown as { totalClicks: number; uniqueVisitors: number }[];
|
||||
return rows[0] ?? { totalClicks: 0, uniqueVisitors: 0 };
|
||||
}
|
||||
|
||||
async getClicksOverTime(linkId: string, days = 30) {
|
||||
const since = new Date();
|
||||
since.setDate(since.getDate() - days);
|
||||
|
||||
return this.db
|
||||
.select({
|
||||
date: sql<string>`date_trunc('day', ${clicks.clickedAt})::date`,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(and(eq(clicks.linkId, linkId), gte(clicks.clickedAt, since)))
|
||||
.groupBy(sql`date_trunc('day', ${clicks.clickedAt})`)
|
||||
.orderBy(sql`date_trunc('day', ${clicks.clickedAt})`);
|
||||
return this.db.execute(sql`
|
||||
SELECT
|
||||
date_trunc('day', created_at)::date::text as date,
|
||||
count(*)::int as count
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload' AND table_name = 'clicks'
|
||||
AND data->>'linkId' = ${linkId}
|
||||
AND op = 'insert'
|
||||
AND created_at >= now() - make_interval(days => ${days})
|
||||
GROUP BY date_trunc('day', created_at)
|
||||
ORDER BY date_trunc('day', created_at)
|
||||
`) as unknown as { date: string; count: number }[];
|
||||
}
|
||||
|
||||
async getTopReferrers(linkId: string, limit = 10) {
|
||||
return this.db
|
||||
.select({
|
||||
referer: clicks.referer,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(eq(clicks.linkId, linkId))
|
||||
.groupBy(clicks.referer)
|
||||
.orderBy(desc(sql`count(*)`))
|
||||
.limit(limit);
|
||||
return this.db.execute(sql`
|
||||
SELECT
|
||||
COALESCE(data->>'referer', 'Direct') as referer,
|
||||
count(*)::int as count
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload' AND table_name = 'clicks'
|
||||
AND data->>'linkId' = ${linkId}
|
||||
AND op = 'insert'
|
||||
GROUP BY data->>'referer'
|
||||
ORDER BY count(*) DESC
|
||||
LIMIT ${limit}
|
||||
`) as unknown as { referer: string; count: number }[];
|
||||
}
|
||||
|
||||
async getDeviceBreakdown(linkId: string) {
|
||||
return this.db
|
||||
.select({
|
||||
deviceType: clicks.deviceType,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(eq(clicks.linkId, linkId))
|
||||
.groupBy(clicks.deviceType)
|
||||
.orderBy(desc(sql`count(*)`));
|
||||
return this.db.execute(sql`
|
||||
SELECT
|
||||
COALESCE(data->>'deviceType', 'Unknown') as "deviceType",
|
||||
count(*)::int as count
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload' AND table_name = 'clicks'
|
||||
AND data->>'linkId' = ${linkId}
|
||||
AND op = 'insert'
|
||||
GROUP BY data->>'deviceType'
|
||||
ORDER BY count(*) DESC
|
||||
`) as unknown as { deviceType: string; count: number }[];
|
||||
}
|
||||
|
||||
async getCountryBreakdown(linkId: string) {
|
||||
return this.db
|
||||
.select({
|
||||
country: clicks.country,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(eq(clicks.linkId, linkId))
|
||||
.groupBy(clicks.country)
|
||||
.orderBy(desc(sql`count(*)`));
|
||||
return this.db.execute(sql`
|
||||
SELECT
|
||||
COALESCE(data->>'country', 'Unknown') as country,
|
||||
count(*)::int as count
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload' AND table_name = 'clicks'
|
||||
AND data->>'linkId' = ${linkId}
|
||||
AND op = 'insert'
|
||||
GROUP BY data->>'country'
|
||||
ORDER BY count(*) DESC
|
||||
`) as unknown as { country: string; count: number }[];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,49 @@
|
|||
import { eq, sql } from 'drizzle-orm';
|
||||
import { links, clicks } from '@manacore/uload-database';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import type { Database } from '../db/connection';
|
||||
|
||||
interface ResolvedLink {
|
||||
id: string;
|
||||
originalUrl: string;
|
||||
isActive: boolean;
|
||||
password: string | null;
|
||||
maxClicks: number | null;
|
||||
clickCount: number;
|
||||
expiresAt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads link data from mana-sync's sync_changes table.
|
||||
* Data is stored as JSONB in the `data` column with app_id='uload' and table_name='links'.
|
||||
* We get the latest version of each record by using DISTINCT ON.
|
||||
*/
|
||||
export class RedirectService {
|
||||
constructor(private db: Database) {}
|
||||
|
||||
async resolve(shortCode: string) {
|
||||
const [link] = await this.db
|
||||
.select({
|
||||
id: links.id,
|
||||
originalUrl: links.originalUrl,
|
||||
isActive: links.isActive,
|
||||
password: links.password,
|
||||
maxClicks: links.maxClicks,
|
||||
clickCount: links.clickCount,
|
||||
expiresAt: links.expiresAt,
|
||||
})
|
||||
.from(links)
|
||||
.where(eq(links.shortCode, shortCode))
|
||||
.limit(1);
|
||||
async resolve(shortCode: string): Promise<ResolvedLink | null> {
|
||||
const result = await this.db.execute(sql`
|
||||
SELECT DISTINCT ON (record_id)
|
||||
record_id as id,
|
||||
data->>'originalUrl' as "originalUrl",
|
||||
COALESCE((data->>'isActive')::boolean, true) as "isActive",
|
||||
data->>'password' as password,
|
||||
(data->>'maxClicks')::int as "maxClicks",
|
||||
COALESCE((data->>'clickCount')::int, 0) as "clickCount",
|
||||
data->>'expiresAt' as "expiresAt"
|
||||
FROM sync_changes
|
||||
WHERE app_id = 'uload'
|
||||
AND table_name = 'links'
|
||||
AND data->>'shortCode' = ${shortCode}
|
||||
AND op != 'delete'
|
||||
ORDER BY record_id, created_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
const rows = result as unknown as ResolvedLink[];
|
||||
const link = rows[0];
|
||||
if (!link) return null;
|
||||
if (!link.isActive) return null;
|
||||
if (link.expiresAt && new Date(link.expiresAt) < new Date()) return null;
|
||||
if (link.maxClicks && link.clickCount && link.clickCount >= link.maxClicks) return null;
|
||||
if (link.maxClicks && link.clickCount >= link.maxClicks) return null;
|
||||
|
||||
return link;
|
||||
}
|
||||
|
|
@ -40,21 +60,24 @@ export class RedirectService {
|
|||
country?: string;
|
||||
}
|
||||
) {
|
||||
await Promise.all([
|
||||
this.db.insert(clicks).values({
|
||||
linkId,
|
||||
ipHash: meta.ipHash,
|
||||
userAgent: meta.userAgent,
|
||||
referer: meta.referer,
|
||||
browser: meta.browser,
|
||||
deviceType: meta.deviceType,
|
||||
os: meta.os,
|
||||
country: meta.country,
|
||||
}),
|
||||
this.db
|
||||
.update(links)
|
||||
.set({ clickCount: sql`${links.clickCount} + 1` })
|
||||
.where(eq(links.id, linkId)),
|
||||
]);
|
||||
const clickId = crypto.randomUUID();
|
||||
const clickData = JSON.stringify({
|
||||
id: clickId,
|
||||
linkId,
|
||||
ipHash: meta.ipHash || null,
|
||||
userAgent: meta.userAgent || null,
|
||||
referer: meta.referer || null,
|
||||
browser: meta.browser || null,
|
||||
deviceType: meta.deviceType || null,
|
||||
os: meta.os || null,
|
||||
country: meta.country || null,
|
||||
clickedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Insert click as a sync_changes record so it's visible to clients
|
||||
await this.db.execute(sql`
|
||||
INSERT INTO sync_changes (app_id, table_name, record_id, user_id, op, data, client_id)
|
||||
VALUES ('uload', 'clicks', ${clickId}, 'system', 'insert', ${clickData}::jsonb, 'uload-server')
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1421,7 +1421,7 @@ services:
|
|||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3041
|
||||
DATABASE_URL: postgresql://manacore:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/uload
|
||||
DATABASE_URL: postgresql://manacore:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/mana_sync
|
||||
MANA_CORE_AUTH_URL: http://mana-auth:3001
|
||||
CORS_ORIGINS: http://uload-web:5029,https://uload.mana.how,https://ulo.ad
|
||||
ports:
|
||||
|
|
|
|||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
|
|
@ -4919,9 +4919,6 @@ importers:
|
|||
|
||||
apps/uload/apps/server:
|
||||
dependencies:
|
||||
'@manacore/uload-database':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/uload-database
|
||||
drizzle-orm:
|
||||
specifier: ^0.44.7
|
||||
version: 0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(bun-types@1.3.11)(expo-sqlite@55.0.10(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(gel@2.2.0)(kysely@0.28.8)(postgres@3.4.7)
|
||||
|
|
@ -4934,6 +4931,9 @@ importers:
|
|||
postgres:
|
||||
specifier: ^3.4.7
|
||||
version: 3.4.7
|
||||
stripe:
|
||||
specifier: ^18.4.0
|
||||
version: 18.5.0(@types/node@24.10.1)
|
||||
devDependencies:
|
||||
'@types/bun':
|
||||
specifier: ^1.2.0
|
||||
|
|
@ -20226,6 +20226,15 @@ packages:
|
|||
resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==}
|
||||
engines: {node: '>=12.*'}
|
||||
|
||||
stripe@18.5.0:
|
||||
resolution: {integrity: sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA==}
|
||||
engines: {node: '>=12.*'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=12.x.x'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
strnum@1.1.2:
|
||||
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
|
||||
|
||||
|
|
@ -44870,6 +44879,12 @@ snapshots:
|
|||
'@types/node': 22.19.1
|
||||
qs: 6.14.0
|
||||
|
||||
stripe@18.5.0(@types/node@24.10.1):
|
||||
dependencies:
|
||||
qs: 6.14.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
strnum@1.1.2: {}
|
||||
|
||||
strnum@2.1.1: {}
|
||||
|
|
|
|||
|
|
@ -192,4 +192,4 @@ services/mana-sync/
|
|||
|
||||
## Connected Apps (19)
|
||||
|
||||
Todo, Calendar, Clock, Contacts, Chat, Questions, Mukke, Context, Photos, ManaDeck, Picture, Presi, Storage, Zitare, SkillTree, CityCorners, NutriPhi, Planta, Inventar
|
||||
Todo, Calendar, Clock, Contacts, Chat, Questions, Mukke, Context, Photos, ManaDeck, Picture, Presi, Storage, Zitare, SkillTree, CityCorners, NutriPhi, Planta, Inventar, uLoad, Taktik, Calc
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue