Compare commits
6 Commits
master
...
db-backup-
Author | SHA1 | Date |
---|---|---|
|
5a6e5289ff | 2 months ago |
|
6620311b7d | 2 months ago |
|
6e2bdc69e9 | 2 months ago |
|
b8c3517072 | 2 months ago |
|
a0cf9ea721 | 2 months ago |
|
42d706b1fb | 2 months ago |
58 changed files with 5698 additions and 1973 deletions
@ -0,0 +1 @@ |
|||
PLATFORM=electron |
@ -0,0 +1,5 @@ |
|||
PLATFORM=capacitor |
|||
VITE_ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim |
|||
VITE_PARTNER_API_URL=https://test-api.partner.ch/api/v2 |
|||
VITE_IMAGE_API_URL=https://test-api.images.ch/api/v2 |
|||
VITE_PUSH_SERVER_URL=https://test-api.push.ch/api/v2 |
@ -0,0 +1 @@ |
|||
PLATFORM=web |
Binary file not shown.
@ -1,2 +1,2 @@ |
|||
#Fri Mar 21 07:27:50 UTC 2025 |
|||
gradle.version=8.2.1 |
|||
#Thu Apr 03 10:21:42 UTC 2025 |
|||
gradle.version=8.11.1 |
|||
|
Binary file not shown.
File diff suppressed because it is too large
@ -1,42 +1,101 @@ |
|||
<!-- eslint-disable-next-line vue/no-v-html --> |
|||
<template> |
|||
<!-- eslint-disable-next-line vue/no-v-html --> |
|||
<div class="w-fit" v-html="generateIcon()"></div> |
|||
<div class="w-fit"> |
|||
<img |
|||
v-if="hasImage" |
|||
:src="imageUrl" |
|||
class="rounded cursor-pointer" |
|||
:width="iconSize" |
|||
:height="iconSize" |
|||
@click="handleClick" |
|||
/> |
|||
<div v-else class="cursor-pointer" @click="handleClick"> |
|||
<img |
|||
v-if="!identifier" |
|||
:src="blankSquareUrl" |
|||
class="rounded" |
|||
:width="iconSize" |
|||
:height="iconSize" |
|||
/> |
|||
<svg |
|||
v-else |
|||
:width="iconSize" |
|||
:height="iconSize" |
|||
viewBox="0 0 100 100" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
> |
|||
<g v-for="(path, index) in avatarPaths" :key="index"> |
|||
<path :d="path" /> |
|||
</g> |
|||
</svg> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { createAvatar, StyleOptions } from "@dicebear/core"; |
|||
import { avataaars } from "@dicebear/collection"; |
|||
import { Vue, Component, Prop } from "vue-facing-decorator"; |
|||
import { Contact } from "../db/tables/contacts"; |
|||
import { logger } from "../utils/logger"; |
|||
|
|||
@Component |
|||
export default class EntityIcon extends Vue { |
|||
@Prop contact: Contact; |
|||
@Prop({ required: false }) contact?: Contact; |
|||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl |
|||
@Prop iconSize = 0; |
|||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl |
|||
|
|||
generateIcon() { |
|||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl; |
|||
if (imageUrl) { |
|||
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`; |
|||
} else { |
|||
const identifier = this.contact?.did || this.entityId; |
|||
if (!identifier) { |
|||
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`; |
|||
} |
|||
// https://api.dicebear.com/8.x/avataaars/svg?seed= |
|||
// ... does not render things with the same seed as this library. |
|||
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring |
|||
// ... which looks similar to '' at the dicebear site but which is different. |
|||
const options: StyleOptions<object> = { |
|||
seed: (identifier as string) || "", |
|||
size: this.iconSize, |
|||
}; |
|||
const avatar = createAvatar(avataaars, options); |
|||
const svgString = avatar.toString(); |
|||
return svgString; |
|||
private avatarPaths: string[] = []; |
|||
private blankSquareUrl = |
|||
import.meta.env.VITE_BASE_URL + "assets/blank-square.svg"; |
|||
|
|||
get imageUrl(): string { |
|||
return this.contact?.profileImageUrl || this.profileImageUrl; |
|||
} |
|||
|
|||
get hasImage(): boolean { |
|||
return !!this.imageUrl; |
|||
} |
|||
|
|||
get identifier(): string | undefined { |
|||
return this.contact?.did || this.entityId; |
|||
} |
|||
|
|||
handleClick() { |
|||
try { |
|||
// Emit a simple event without passing the event object |
|||
this.$emit("click"); |
|||
} catch (error) { |
|||
logger.error("Error handling click event:", error); |
|||
} |
|||
} |
|||
|
|||
generateAvatarPaths(): string[] { |
|||
if (!this.identifier) return []; |
|||
|
|||
const options: StyleOptions<object> = { |
|||
seed: this.identifier, |
|||
size: this.iconSize, |
|||
}; |
|||
const avatar = createAvatar(avataaars, options); |
|||
const svgString = avatar.toString(); |
|||
|
|||
// Extract paths from SVG string |
|||
const parser = new DOMParser(); |
|||
const doc = parser.parseFromString(svgString, "image/svg+xml"); |
|||
const paths = Array.from(doc.querySelectorAll("path")).map( |
|||
(path) => path.getAttribute("d") || "", |
|||
); |
|||
return paths; |
|||
} |
|||
|
|||
mounted() { |
|||
this.avatarPaths = this.generateAvatarPaths(); |
|||
logger.log("EntityIcon mounted, profileImageUrl:", this.profileImageUrl); |
|||
logger.log("EntityIcon mounted, entityId:", this.entityId); |
|||
logger.log("EntityIcon mounted, iconSize:", this.iconSize); |
|||
} |
|||
} |
|||
</script> |
|||
<style scoped></style> |
|||
|
@ -0,0 +1,257 @@ |
|||
/** * @file ProfileSection.vue * @description Component for managing user |
|||
profile information * @author Matthew Raymer * @version 1.0.0 */ |
|||
|
|||
<template> |
|||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> |
|||
<div v-if="loading" class="text-center mb-2"> |
|||
<font-awesome |
|||
icon="spinner" |
|||
class="fa-spin text-slate-400" |
|||
></font-awesome> |
|||
Loading profile... |
|||
</div> |
|||
<div v-else class="flex items-center mb-2"> |
|||
<span class="font-bold">Public Profile</span> |
|||
<font-awesome |
|||
icon="circle-info" |
|||
class="text-slate-400 fa-fw ml-2 cursor-pointer" |
|||
@click="showProfileInfo" |
|||
/> |
|||
</div> |
|||
<textarea |
|||
v-model="profileDesc" |
|||
class="w-full h-32 p-2 border border-slate-300 rounded-md" |
|||
placeholder="Write something about yourself for the public..." |
|||
:readonly="loading || saving" |
|||
:class="{ 'bg-slate-100': loading || saving }" |
|||
></textarea> |
|||
|
|||
<div class="flex items-center mb-4" @click="toggleLocation"> |
|||
<input v-model="includeLocation" type="checkbox" class="mr-2" /> |
|||
<label for="includeLocation">Include Location</label> |
|||
</div> |
|||
<div v-if="includeLocation" class="mb-4 aspect-video"> |
|||
<p class="text-sm mb-2 text-slate-500"> |
|||
For your security, choose a location nearby but not exactly at your |
|||
place. |
|||
</p> |
|||
|
|||
<l-map |
|||
ref="profileMap" |
|||
class="!z-40 rounded-md" |
|||
@click="handleMapClick" |
|||
@ready="onMapReady" |
|||
> |
|||
<l-tile-layer |
|||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" |
|||
layer-type="base" |
|||
name="OpenStreetMap" |
|||
/> |
|||
<l-marker |
|||
v-if="latitude && longitude" |
|||
:lat-lng="[latitude, longitude]" |
|||
@click="confirmEraseLocation" |
|||
/> |
|||
</l-map> |
|||
</div> |
|||
<div v-if="!loading && !saving"> |
|||
<div class="flex justify-between items-center"> |
|||
<button |
|||
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" |
|||
:disabled="loading || saving" |
|||
:class="{ |
|||
'opacity-50 cursor-not-allowed': loading || saving, |
|||
}" |
|||
@click="saveProfile" |
|||
> |
|||
Save Profile |
|||
</button> |
|||
<button |
|||
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" |
|||
:disabled="loading || saving" |
|||
:class="{ |
|||
'opacity-50 cursor-not-allowed': |
|||
loading || saving || (!profileDesc && !includeLocation), |
|||
}" |
|||
@click="confirmDeleteProfile" |
|||
> |
|||
Delete Profile |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div v-else-if="loading">Loading...</div> |
|||
<div v-else>Saving...</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; |
|||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; |
|||
import { ProfileService } from "../services/ProfileService"; |
|||
import { logger } from "../utils/logger"; |
|||
|
|||
@Component({ |
|||
components: { |
|||
LMap, |
|||
LMarker, |
|||
LTileLayer, |
|||
}, |
|||
}) |
|||
export default class ProfileSection extends Vue { |
|||
@Prop({ required: true }) activeDid!: string; |
|||
@Prop({ required: true }) partnerApiServer!: string; |
|||
|
|||
@Emit("profile-updated") profileUpdated() {} |
|||
|
|||
loading = true; |
|||
saving = false; |
|||
profileDesc = ""; |
|||
latitude = 0; |
|||
longitude = 0; |
|||
includeLocation = false; |
|||
zoom = 2; |
|||
|
|||
async mounted() { |
|||
await this.loadProfile(); |
|||
} |
|||
|
|||
async loadProfile() { |
|||
try { |
|||
const profile = await ProfileService.loadProfile( |
|||
this.activeDid, |
|||
this.partnerApiServer, |
|||
); |
|||
if (profile) { |
|||
this.profileDesc = profile.description || ""; |
|||
this.latitude = profile.location?.lat || 0; |
|||
this.longitude = profile.location?.lng || 0; |
|||
this.includeLocation = !!(this.latitude && this.longitude); |
|||
} |
|||
} catch (error) { |
|||
logger.error("Error loading profile:", error); |
|||
this.$notify({ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error Loading Profile", |
|||
text: "Your server profile is not available.", |
|||
}); |
|||
} finally { |
|||
this.loading = false; |
|||
} |
|||
} |
|||
|
|||
async saveProfile() { |
|||
this.saving = true; |
|||
try { |
|||
await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, { |
|||
description: this.profileDesc, |
|||
location: this.includeLocation |
|||
? { |
|||
lat: this.latitude, |
|||
lng: this.longitude, |
|||
} |
|||
: undefined, |
|||
}); |
|||
|
|||
this.$notify({ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Profile Saved", |
|||
text: "Your profile has been updated successfully.", |
|||
}); |
|||
this.profileUpdated(); |
|||
} catch (error) { |
|||
logger.error("Error saving profile:", error); |
|||
this.$notify({ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error Saving Profile", |
|||
text: "There was an error saving your profile.", |
|||
}); |
|||
} finally { |
|||
this.saving = false; |
|||
} |
|||
} |
|||
|
|||
toggleLocation() { |
|||
this.includeLocation = !this.includeLocation; |
|||
if (!this.includeLocation) { |
|||
this.latitude = 0; |
|||
this.longitude = 0; |
|||
this.zoom = 2; |
|||
} |
|||
} |
|||
|
|||
handleMapClick(event: { latlng: { lat: number; lng: number } }) { |
|||
this.latitude = event.latlng.lat; |
|||
this.longitude = event.latlng.lng; |
|||
} |
|||
|
|||
onMapReady(map: L.Map) { |
|||
const zoom = this.latitude && this.longitude ? 12 : 2; |
|||
map.setView([this.latitude, this.longitude], zoom); |
|||
} |
|||
|
|||
confirmEraseLocation() { |
|||
this.$notify({ |
|||
group: "modal", |
|||
type: "confirm", |
|||
title: "Erase Marker", |
|||
text: "Are you sure you don't want to mark a location? This will erase the current location.", |
|||
onYes: () => { |
|||
this.latitude = 0; |
|||
this.longitude = 0; |
|||
this.zoom = 2; |
|||
this.includeLocation = false; |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
async confirmDeleteProfile() { |
|||
this.$notify({ |
|||
group: "modal", |
|||
type: "confirm", |
|||
title: "Delete Profile", |
|||
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.", |
|||
onYes: this.deleteProfile, |
|||
}); |
|||
} |
|||
|
|||
async deleteProfile() { |
|||
this.saving = true; |
|||
try { |
|||
await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer); |
|||
this.profileDesc = ""; |
|||
this.latitude = 0; |
|||
this.longitude = 0; |
|||
this.includeLocation = false; |
|||
this.$notify({ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Profile Deleted", |
|||
text: "Your profile has been deleted successfully.", |
|||
}); |
|||
this.profileUpdated(); |
|||
} catch (error) { |
|||
logger.error("Error deleting profile:", error); |
|||
this.$notify({ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error Deleting Profile", |
|||
text: "There was an error deleting your profile.", |
|||
}); |
|||
} finally { |
|||
this.saving = false; |
|||
} |
|||
} |
|||
|
|||
showProfileInfo() { |
|||
this.$notify({ |
|||
group: "alert", |
|||
type: "info", |
|||
title: "Public Profile Information", |
|||
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.", |
|||
}); |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Interface for a Decentralized Identifier (DID) |
|||
* @author Matthew Raymer |
|||
*/ |
|||
|
|||
import { KeyMeta } from "../libs/crypto/vc"; |
|||
|
|||
export interface IKey { |
|||
id: string; |
|||
type: string; |
|||
controller: string; |
|||
ethereumAddress: string; |
|||
publicKeyHex: string; |
|||
privateKeyHex: string; |
|||
meta?: KeyMeta; |
|||
} |
|||
|
|||
export interface IService { |
|||
id: string; |
|||
type: string; |
|||
serviceEndpoint: string; |
|||
description?: string; |
|||
metadata?: { |
|||
version?: string; |
|||
capabilities?: string[]; |
|||
config?: Record<string, unknown>; |
|||
}; |
|||
} |
|||
|
|||
export interface IIdentifier { |
|||
did: string; |
|||
keys: IKey[]; |
|||
services: IService[]; |
|||
} |
@ -0,0 +1,288 @@ |
|||
/** |
|||
* @file service.ts |
|||
* @description Service interfaces for Decentralized Identifiers (DIDs) |
|||
* |
|||
* This module defines the service interfaces used in the TimeSafari application. |
|||
* Services are associated with DIDs to provide additional functionality and endpoints. |
|||
* |
|||
* Architecture: |
|||
* 1. Base IService interface defines common service properties |
|||
* 2. Specialized interfaces extend IService for specific service types |
|||
* 3. Services are stored in IIdentifier.services array |
|||
* 4. Services are loaded and managed by PlatformServiceFactory |
|||
* |
|||
* Service Types: |
|||
* - EndorserService: Handles claims and endorsements |
|||
* - PushNotificationService: Manages web push notifications |
|||
* - ProfileService: Handles user profiles and settings |
|||
* - BackupService: Manages data backup and restore |
|||
* |
|||
* @see IIdentifier |
|||
* @see PlatformServiceFactory |
|||
* @see DatabaseBackupService |
|||
*/ |
|||
|
|||
/** |
|||
* Base interface for all DID services |
|||
* |
|||
* This interface defines the core properties that all services must implement. |
|||
* It follows the W3C DID specification for service endpoints. |
|||
* |
|||
* @example |
|||
* const service: IService = { |
|||
* id: 'endorser-service', |
|||
* type: 'EndorserService', |
|||
* serviceEndpoint: 'https://api.endorser.ch', |
|||
* description: 'Endorser service for claims and endorsements', |
|||
* metadata: { |
|||
* version: '1.0.0', |
|||
* capabilities: ['claims', 'endorsements'], |
|||
* config: { apiServer: 'https://api.endorser.ch' } |
|||
* } |
|||
* }; |
|||
*/ |
|||
export interface IService { |
|||
/** |
|||
* Unique identifier for the service |
|||
* @example 'endorser-service' |
|||
* @example 'push-notification-service' |
|||
*/ |
|||
id: string; |
|||
|
|||
/** |
|||
* Type of service |
|||
* @example 'EndorserService' |
|||
* @example 'PushNotificationService' |
|||
*/ |
|||
type: string; |
|||
|
|||
/** |
|||
* Endpoint URL for the service |
|||
* @example 'https://api.endorser.ch' |
|||
* @example 'https://push.timesafari.app' |
|||
*/ |
|||
serviceEndpoint: string; |
|||
|
|||
/** |
|||
* Optional human-readable description of the service |
|||
* @example 'Service for handling claims and endorsements' |
|||
*/ |
|||
description?: string; |
|||
|
|||
/** |
|||
* Optional metadata for service configuration |
|||
*/ |
|||
metadata?: { |
|||
/** |
|||
* Service version in semantic versioning format |
|||
* @example '1.0.0' |
|||
*/ |
|||
version?: string; |
|||
|
|||
/** |
|||
* Array of service capabilities |
|||
* @example ['claims', 'endorsements'] |
|||
* @example ['notifications', 'alerts'] |
|||
*/ |
|||
capabilities?: string[]; |
|||
|
|||
/** |
|||
* Service-specific configuration |
|||
* @example { apiServer: 'https://api.endorser.ch' } |
|||
*/ |
|||
config?: Record<string, unknown>; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Service for handling claims and endorsements |
|||
* |
|||
* This service provides endpoints for: |
|||
* - Submitting claims |
|||
* - Managing endorsements |
|||
* - Checking rate limits |
|||
* |
|||
* @example |
|||
* const endorserService: IEndorserService = { |
|||
* id: 'endorser-service', |
|||
* type: 'EndorserService', |
|||
* serviceEndpoint: 'https://api.endorser.ch', |
|||
* metadata: { |
|||
* version: '1.0.0', |
|||
* capabilities: ['claims', 'endorsements'], |
|||
* config: { |
|||
* apiServer: 'https://api.endorser.ch', |
|||
* rateLimits: { |
|||
* claimsPerDay: 100, |
|||
* endorsementsPerDay: 1000 |
|||
* } |
|||
* } |
|||
* } |
|||
* }; |
|||
*/ |
|||
export interface IEndorserService extends IService { |
|||
/** @override */ |
|||
type: "EndorserService"; |
|||
|
|||
/** @override */ |
|||
metadata: { |
|||
version: string; |
|||
capabilities: ["claims", "endorsements"]; |
|||
config: { |
|||
/** |
|||
* API server URL |
|||
* @example 'https://api.endorser.ch' |
|||
*/ |
|||
apiServer: string; |
|||
|
|||
/** |
|||
* Optional rate limits |
|||
*/ |
|||
rateLimits?: { |
|||
/** |
|||
* Maximum claims per day |
|||
* @default 100 |
|||
*/ |
|||
claimsPerDay: number; |
|||
|
|||
/** |
|||
* Maximum endorsements per day |
|||
* @default 1000 |
|||
*/ |
|||
endorsementsPerDay: number; |
|||
}; |
|||
}; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Service for managing web push notifications |
|||
* |
|||
* This service provides endpoints for: |
|||
* - Registering push subscriptions |
|||
* - Sending push notifications |
|||
* - Managing notification preferences |
|||
* |
|||
* @example |
|||
* const pushService: IPushNotificationService = { |
|||
* id: 'push-service', |
|||
* type: 'PushNotificationService', |
|||
* serviceEndpoint: 'https://push.timesafari.app', |
|||
* metadata: { |
|||
* version: '1.0.0', |
|||
* capabilities: ['notifications'], |
|||
* config: { |
|||
* pushServer: 'https://push.timesafari.app', |
|||
* vapidPublicKey: '...' |
|||
* } |
|||
* } |
|||
* }; |
|||
*/ |
|||
export interface IPushNotificationService extends IService { |
|||
/** @override */ |
|||
type: "PushNotificationService"; |
|||
|
|||
/** @override */ |
|||
metadata: { |
|||
version: string; |
|||
capabilities: ["notifications"]; |
|||
config: { |
|||
/** |
|||
* Push server URL |
|||
* @example 'https://push.timesafari.app' |
|||
*/ |
|||
pushServer: string; |
|||
|
|||
/** |
|||
* Optional VAPID public key for push notifications |
|||
*/ |
|||
vapidPublicKey?: string; |
|||
}; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Service for managing user profiles and settings |
|||
* |
|||
* This service provides endpoints for: |
|||
* - Managing user profiles |
|||
* - Updating user settings |
|||
* - Retrieving user preferences |
|||
* |
|||
* @example |
|||
* const profileService: IProfileService = { |
|||
* id: 'profile-service', |
|||
* type: 'ProfileService', |
|||
* serviceEndpoint: 'https://partner-api.endorser.ch', |
|||
* metadata: { |
|||
* version: '1.0.0', |
|||
* capabilities: ['profile', 'settings'], |
|||
* config: { |
|||
* partnerApiServer: 'https://partner-api.endorser.ch' |
|||
* } |
|||
* } |
|||
* }; |
|||
*/ |
|||
export interface IProfileService extends IService { |
|||
/** @override */ |
|||
type: "ProfileService"; |
|||
|
|||
/** @override */ |
|||
metadata: { |
|||
version: string; |
|||
capabilities: ["profile", "settings"]; |
|||
config: { |
|||
/** |
|||
* Partner API server URL |
|||
* @example 'https://partner-api.endorser.ch' |
|||
*/ |
|||
partnerApiServer: string; |
|||
}; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Service for managing data backup and restore operations |
|||
* |
|||
* This service provides endpoints for: |
|||
* - Creating backups |
|||
* - Restoring from backups |
|||
* - Managing backup storage |
|||
* |
|||
* @example |
|||
* const backupService: IBackupService = { |
|||
* id: 'backup-service', |
|||
* type: 'BackupService', |
|||
* serviceEndpoint: 'https://backup.timesafari.app', |
|||
* metadata: { |
|||
* version: '1.0.0', |
|||
* capabilities: ['backup', 'restore'], |
|||
* config: { |
|||
* storageType: 'cloud', |
|||
* encryptionKey: '...' |
|||
* } |
|||
* } |
|||
* }; |
|||
*/ |
|||
export interface IBackupService extends IService { |
|||
/** @override */ |
|||
type: "BackupService"; |
|||
|
|||
/** @override */ |
|||
metadata: { |
|||
version: string; |
|||
capabilities: ["backup", "restore"]; |
|||
config: { |
|||
/** |
|||
* Storage type for backups |
|||
* @default 'local' |
|||
*/ |
|||
storageType: "local" | "cloud"; |
|||
|
|||
/** |
|||
* Optional encryption key for backups |
|||
*/ |
|||
encryptionKey?: string; |
|||
}; |
|||
}; |
|||
} |
@ -0,0 +1,82 @@ |
|||
/** |
|||
* @file DatabaseBackupService.ts |
|||
* @description Capacitor-specific implementation of database backup service |
|||
* |
|||
* This service handles database backup operations on Capacitor platforms (Android/iOS) |
|||
* using the Filesystem and Share plugins. It creates a temporary backup file, |
|||
* writes the backup data to it, and shares the file using the platform's share sheet. |
|||
*/ |
|||
|
|||
import { Filesystem, Directory } from "@capacitor/filesystem"; |
|||
import { Share } from "@capacitor/share"; |
|||
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService"; |
|||
import { log, error } from "../../utils/logger"; |
|||
|
|||
export class DatabaseBackupService extends BaseDatabaseBackupService { |
|||
/** |
|||
* Handles the backup process for Capacitor platforms |
|||
* |
|||
* @param base64Data - Backup data in base64 format |
|||
* @param arrayBuffer - Backup data as ArrayBuffer |
|||
* @param blob - Backup data as Blob |
|||
*/ |
|||
protected async handleBackup( |
|||
base64Data: string, |
|||
_arrayBuffer: ArrayBuffer, |
|||
_blob: Blob, |
|||
): Promise<void> { |
|||
try { |
|||
log("Starting backup process for Capacitor platform"); |
|||
|
|||
// Create a timestamped backup file name
|
|||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); |
|||
const backupFileName = `timesafari-backup-${timestamp}.json`; |
|||
const backupFilePath = `backups/${backupFileName}`; |
|||
|
|||
log("Creating backup file:", { |
|||
fileName: backupFileName, |
|||
path: backupFilePath, |
|||
}); |
|||
|
|||
// Write the backup file
|
|||
const writeResult = (await Filesystem.writeFile({ |
|||
path: backupFilePath, |
|||
data: base64Data, |
|||
directory: Directory.Cache, |
|||
recursive: true, |
|||
})) as unknown as { uri: string }; |
|||
|
|||
if (!writeResult.uri) { |
|||
throw new Error("Failed to write backup file: No URI returned"); |
|||
} |
|||
|
|||
log("Backup file written successfully:", { uri: writeResult.uri }); |
|||
|
|||
// Share the backup file
|
|||
log("Sharing backup file"); |
|||
await Share.share({ |
|||
title: "TimeSafari Backup", |
|||
text: "Your TimeSafari backup file", |
|||
url: writeResult.uri, |
|||
dialogTitle: "Share TimeSafari Backup", |
|||
}); |
|||
|
|||
log("Backup shared successfully"); |
|||
|
|||
// Clean up the temporary file
|
|||
try { |
|||
await Filesystem.deleteFile({ |
|||
path: backupFilePath, |
|||
directory: Directory.Cache, |
|||
}); |
|||
log("Temporary backup file cleaned up"); |
|||
} catch (cleanupError) { |
|||
error("Failed to clean up temporary backup file:", cleanupError); |
|||
// Don't throw here as the backup was successful
|
|||
} |
|||
} catch (err) { |
|||
error("Error during backup process:", err); |
|||
throw err; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,95 @@ |
|||
/** |
|||
* @file DatabaseBackupService.ts |
|||
* @description Base service class for handling database backup operations |
|||
* |
|||
* This service implements the Template Method pattern to provide a common interface |
|||
* for database backup operations across different platforms. It defines the structure |
|||
* of backup operations while delegating platform-specific implementations to subclasses. |
|||
* |
|||
* Build Process Integration: |
|||
* 1. Platform-Specific Implementation: |
|||
* - Each platform (web, electron, capacitor) has its own implementation |
|||
* - Implementations are loaded dynamically via PlatformServiceFactory |
|||
* - Located in ./platforms/{platform}/DatabaseBackupService.ts |
|||
* |
|||
* 2. Build Configuration: |
|||
* - Vite config files (vite.config.*.mts) set VITE_PLATFORM |
|||
* - PlatformServiceFactory uses this to load correct implementation |
|||
* - Build process creates separate chunks for each platform |
|||
* |
|||
* 3. Data Handling: |
|||
* - Supports multiple data formats (base64, ArrayBuffer, Blob) |
|||
* - Platform implementations handle format conversion |
|||
* - Ensures consistent backup format across platforms |
|||
* |
|||
* Usage: |
|||
* - Create backup: DatabaseBackupService.createAndShareBackup(data) |
|||
* - Platform-specific: new WebDatabaseBackupService().handleBackup() |
|||
* |
|||
* @see PlatformServiceFactory.ts |
|||
* @see vite.config.web.mts |
|||
* @see vite.config.electron.mts |
|||
* @see vite.config.capacitor.mts |
|||
*/ |
|||
|
|||
import { PlatformServiceFactory } from "./PlatformServiceFactory"; |
|||
import { log, error } from "../utils/logger"; |
|||
|
|||
export class DatabaseBackupService { |
|||
/** |
|||
* Template method that must be implemented by platform-specific services |
|||
* @param base64Data - Backup data in base64 format |
|||
* @param arrayBuffer - Backup data as ArrayBuffer |
|||
* @param blob - Backup data as Blob |
|||
* @throws Error if not implemented by subclass |
|||
*/ |
|||
protected async handleBackup( |
|||
_base64Data: string, |
|||
_arrayBuffer: ArrayBuffer, |
|||
_blob: Blob, |
|||
): Promise<void> { |
|||
throw new Error( |
|||
"handleBackup must be implemented by platform-specific service", |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Factory method to create and share a backup |
|||
* Uses PlatformServiceFactory to get platform-specific implementation |
|||
* |
|||
* @param base64Data - Backup data in base64 format |
|||
* @param arrayBuffer - Backup data as ArrayBuffer |
|||
* @param blob - Backup data as Blob |
|||
* @returns Promise that resolves when backup is complete |
|||
*/ |
|||
public static async createAndShareBackup( |
|||
base64Data: string, |
|||
arrayBuffer: ArrayBuffer, |
|||
blob: Blob, |
|||
): Promise<void> { |
|||
try { |
|||
log("Creating platform-specific backup service"); |
|||
const backupService = await this.getPlatformSpecificBackupService(); |
|||
log("Backup service created successfully"); |
|||
|
|||
log("Executing platform-specific backup"); |
|||
await backupService.handleBackup(base64Data, arrayBuffer, blob); |
|||
log("Backup completed successfully"); |
|||
} catch (err) { |
|||
error("Error during backup creation:", err); |
|||
if (err instanceof Error) { |
|||
error("Error details:", { |
|||
name: err.name, |
|||
message: err.message, |
|||
stack: err.stack, |
|||
}); |
|||
} |
|||
throw err; |
|||
} |
|||
} |
|||
|
|||
private static async getPlatformSpecificBackupService(): Promise<DatabaseBackupService> { |
|||
const factory = PlatformServiceFactory.getInstance(); |
|||
return await factory.createDatabaseBackupService(); |
|||
} |
|||
} |
@ -0,0 +1,195 @@ |
|||
/** |
|||
* @file PlatformServiceFactory.ts |
|||
* @description Factory for creating platform-specific service implementations |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
* |
|||
* This factory implements the Abstract Factory pattern to create platform-specific |
|||
* implementations of services. It uses Vite's dynamic import feature to load the |
|||
* appropriate implementation based on the current platform (web, electron, etc.). |
|||
* |
|||
* Architecture: |
|||
* 1. Singleton Pattern: |
|||
* - Ensures only one factory instance exists |
|||
* - Manages platform-specific service instances |
|||
* - Maintains consistent state across the application |
|||
* |
|||
* 2. Dynamic Loading: |
|||
* - Uses Vite's dynamic import for platform-specific code |
|||
* - Loads services on-demand based on platform |
|||
* - Handles platform detection and service instantiation |
|||
* |
|||
* 3. Platform Detection: |
|||
* - Uses VITE_PLATFORM environment variable |
|||
* - Supports web, electron, and capacitor platforms |
|||
* - Falls back to 'web' if platform is not specified |
|||
* |
|||
* Usage: |
|||
* ```typescript
|
|||
* // Get factory instance
|
|||
* const factory = PlatformServiceFactory.getInstance(); |
|||
* |
|||
* // Create platform-specific service
|
|||
* const backupService = await factory.createDatabaseBackupService(); |
|||
* ``` |
|||
* |
|||
* @see vite.config.web.mts |
|||
* @see vite.config.electron.mts |
|||
* @see vite.config.capacitor.mts |
|||
* @see DatabaseBackupService |
|||
*/ |
|||
|
|||
import { DatabaseBackupService } from "./DatabaseBackupService"; |
|||
import { DatabaseBackupService as StubDatabaseBackupService } from "./platforms/empty"; |
|||
import { logger } from "../utils/logger"; |
|||
|
|||
/** |
|||
* Factory class for creating platform-specific service implementations |
|||
* |
|||
* This class manages the creation and instantiation of platform-specific |
|||
* service implementations. It uses the Abstract Factory pattern to provide |
|||
* a consistent interface for creating services across different platforms. |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* // Get factory instance
|
|||
* const factory = PlatformServiceFactory.getInstance(); |
|||
* |
|||
* // Create platform-specific service
|
|||
* const backupService = await factory.createDatabaseBackupService(); |
|||
* |
|||
* // Use the service
|
|||
* await backupService.handleBackup(data); |
|||
* ``` |
|||
*/ |
|||
export class PlatformServiceFactory { |
|||
/** |
|||
* Singleton instance of the factory |
|||
* @private |
|||
*/ |
|||
private static instance: PlatformServiceFactory; |
|||
|
|||
/** |
|||
* Current platform identifier |
|||
* @private |
|||
*/ |
|||
private platform: string; |
|||
|
|||
/** |
|||
* Private constructor to enforce singleton pattern |
|||
* |
|||
* Initializes the factory with the current platform from environment variables. |
|||
* Falls back to 'web' if no platform is specified. |
|||
* |
|||
* @private |
|||
*/ |
|||
private constructor() { |
|||
this.platform = import.meta.env.VITE_PLATFORM || "web"; |
|||
} |
|||
|
|||
/** |
|||
* Gets the singleton instance of the factory |
|||
* |
|||
* Creates a new instance if one doesn't exist, otherwise returns |
|||
* the existing instance. |
|||
* |
|||
* @returns {PlatformServiceFactory} The singleton factory instance |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const factory = PlatformServiceFactory.getInstance(); |
|||
* ``` |
|||
*/ |
|||
public static getInstance(): PlatformServiceFactory { |
|||
if (!PlatformServiceFactory.instance) { |
|||
PlatformServiceFactory.instance = new PlatformServiceFactory(); |
|||
} |
|||
return PlatformServiceFactory.instance; |
|||
} |
|||
|
|||
/** |
|||
* Creates a platform-specific database backup service |
|||
* |
|||
* Dynamically loads and instantiates the appropriate implementation |
|||
* based on the current platform. The implementation is loaded from |
|||
* the platforms/{platform}/DatabaseBackupService.ts file. |
|||
* |
|||
* @returns {Promise<DatabaseBackupService>} A promise that resolves to a platform-specific backup service |
|||
* @throws {Error} If the service fails to load or instantiate |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const factory = PlatformServiceFactory.getInstance(); |
|||
* try { |
|||
* const backupService = await factory.createDatabaseBackupService(); |
|||
* await backupService.handleBackup(data); |
|||
* } catch (error) { |
|||
* logger.error('Failed to create backup service:', error); |
|||
* } |
|||
* ``` |
|||
*/ |
|||
public async createDatabaseBackupService(): Promise<DatabaseBackupService> { |
|||
// List of supported platforms for web builds
|
|||
const webSupportedPlatforms = ["web", "capacitor", "electron"]; |
|||
|
|||
// Return stub implementation for unsupported platforms
|
|||
if (!webSupportedPlatforms.includes(this.platform)) { |
|||
logger.log( |
|||
`Using stub implementation for unsupported platform: ${this.platform}`, |
|||
); |
|||
return new StubDatabaseBackupService(); |
|||
} |
|||
|
|||
try { |
|||
logger.log(`Loading platform-specific service for ${this.platform}`); |
|||
// Use dynamic import with platform-specific path
|
|||
const module = await import( |
|||
/* @vite-ignore */ |
|||
`./platforms/${this.platform}/DatabaseBackupService.ts` |
|||
); |
|||
logger.log("Platform service loaded successfully"); |
|||
return new module.DatabaseBackupService(); |
|||
} catch (error) { |
|||
logger.error( |
|||
`[TimeSafari] Failed to load platform-specific service for ${this.platform}:`, |
|||
error, |
|||
); |
|||
// Fallback to stub implementation on error
|
|||
logger.log("Falling back to stub implementation"); |
|||
return new StubDatabaseBackupService(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets the current platform identifier |
|||
* |
|||
* @returns {string} The current platform identifier |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const factory = PlatformServiceFactory.getInstance(); |
|||
* logger.log(factory.getPlatform()); // 'web', 'electron', or 'capacitor'
|
|||
* ``` |
|||
*/ |
|||
public getPlatform(): string { |
|||
return this.platform; |
|||
} |
|||
|
|||
/** |
|||
* Sets the current platform identifier |
|||
* |
|||
* This method is primarily used for testing purposes to override |
|||
* the platform detection. Use with caution in production code. |
|||
* |
|||
* @param {string} platform - The platform identifier to set |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const factory = PlatformServiceFactory.getInstance(); |
|||
* factory.setPlatform('electron'); // For testing purposes only
|
|||
* ``` |
|||
*/ |
|||
public setPlatform(platform: string): void { |
|||
this.platform = platform; |
|||
} |
|||
} |
@ -0,0 +1,105 @@ |
|||
/** |
|||
* @file ProfileService.ts |
|||
* @description Service class for handling user profile operations |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { logger } from "../utils/logger"; |
|||
import { getHeaders } from "../libs/endorserServer"; |
|||
import type { UserProfile } from "@/types/interfaces"; |
|||
|
|||
export class ProfileService { |
|||
/** |
|||
* Saves a user profile to the server |
|||
* @param activeDid - The user's active DID |
|||
* @param partnerApiServer - The partner API server URL |
|||
* @param profile - The profile data to save |
|||
* @returns Promise<void> |
|||
*/ |
|||
static async saveProfile( |
|||
activeDid: string, |
|||
partnerApiServer: string, |
|||
profile: Partial<UserProfile>, |
|||
): Promise<void> { |
|||
try { |
|||
const headers = await getHeaders(activeDid); |
|||
const response = await fetch( |
|||
`${partnerApiServer}/api/partner/userProfile`, |
|||
{ |
|||
method: "POST", |
|||
headers, |
|||
body: JSON.stringify(profile), |
|||
}, |
|||
); |
|||
|
|||
if (!response.ok) { |
|||
throw new Error(`Failed to save profile: ${response.statusText}`); |
|||
} |
|||
} catch (error) { |
|||
logger.error("Error saving profile:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Deletes a user profile from the server |
|||
* @param activeDid - The user's active DID |
|||
* @param partnerApiServer - The partner API server URL |
|||
* @returns Promise<void> |
|||
*/ |
|||
static async deleteProfile( |
|||
activeDid: string, |
|||
partnerApiServer: string, |
|||
): Promise<void> { |
|||
try { |
|||
const headers = await getHeaders(activeDid); |
|||
const response = await fetch( |
|||
`${partnerApiServer}/api/partner/userProfile`, |
|||
{ |
|||
method: "DELETE", |
|||
headers, |
|||
}, |
|||
); |
|||
|
|||
if (!response.ok) { |
|||
throw new Error(`Failed to delete profile: ${response.statusText}`); |
|||
} |
|||
} catch (error) { |
|||
logger.error("Error deleting profile:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Loads a user profile from the server |
|||
* @param activeDid - The user's active DID |
|||
* @param partnerApiServer - The partner API server URL |
|||
* @returns Promise<UserProfile | null> |
|||
*/ |
|||
static async loadProfile( |
|||
activeDid: string, |
|||
partnerApiServer: string, |
|||
): Promise<UserProfile | null> { |
|||
try { |
|||
const headers = await getHeaders(activeDid); |
|||
const response = await fetch( |
|||
`${partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`, |
|||
{ headers }, |
|||
); |
|||
|
|||
if (response.status === 404) { |
|||
return null; |
|||
} |
|||
|
|||
if (!response.ok) { |
|||
throw new Error(`Failed to load profile: ${response.statusText}`); |
|||
} |
|||
|
|||
return await response.json(); |
|||
} catch (error) { |
|||
logger.error("Error loading profile:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,110 @@ |
|||
/** |
|||
* @file RateLimitsService.ts |
|||
* @description Service class for handling rate limit operations |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { logger } from "../utils/logger"; |
|||
import { getHeaders } from "../libs/endorserServer"; |
|||
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits"; |
|||
import axios from "axios"; |
|||
|
|||
export class RateLimitsService { |
|||
/** |
|||
* Fetches rate limits for a given DID |
|||
* @param apiServer - The API server URL |
|||
* @param did - The user's DID |
|||
* @returns Promise<EndorserRateLimits> |
|||
*/ |
|||
static async fetchRateLimits( |
|||
apiServer: string, |
|||
did: string, |
|||
): Promise<EndorserRateLimits> { |
|||
logger.log("Fetching rate limits for DID:", did); |
|||
logger.log("Using API server:", apiServer); |
|||
|
|||
try { |
|||
const headers = await getHeaders(did); |
|||
const response = await axios.get( |
|||
`${apiServer}/api/v2/rate-limits/${did}`, |
|||
{ headers }, |
|||
); |
|||
logger.log("Rate limits response:", response.data); |
|||
return response.data; |
|||
} catch (error) { |
|||
if ( |
|||
axios.isAxiosError(error) && |
|||
(error.response?.status === 400 || error.response?.status === 404) |
|||
) { |
|||
const errorData = error.response.data as { |
|||
error?: { message?: string; code?: string }; |
|||
}; |
|||
if ( |
|||
errorData.error?.code === "UNREGISTERED_USER" || |
|||
error.response?.status === 404 |
|||
) { |
|||
logger.log("User is not registered, returning default limits"); |
|||
return { |
|||
doneClaimsThisWeek: "0", |
|||
maxClaimsPerWeek: "0", |
|||
nextWeekBeginDateTime: new Date().toISOString(), |
|||
doneRegistrationsThisMonth: "0", |
|||
maxRegistrationsPerMonth: "0", |
|||
nextMonthBeginDateTime: new Date().toISOString(), |
|||
}; |
|||
} |
|||
} |
|||
logger.error("Error fetching rate limits:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Fetches image rate limits for a given DID |
|||
* @param apiServer - The API server URL |
|||
* @param activeDid - The user's active DID |
|||
* @returns Promise<ImageRateLimits> |
|||
*/ |
|||
static async fetchImageRateLimits( |
|||
apiServer: string, |
|||
activeDid: string, |
|||
): Promise<ImageRateLimits> { |
|||
try { |
|||
const headers = await getHeaders(activeDid); |
|||
const response = await fetch( |
|||
`${apiServer}/api/endorser/imageRateLimits/${activeDid}`, |
|||
{ headers }, |
|||
); |
|||
|
|||
if (!response.ok) { |
|||
throw new Error( |
|||
`Failed to fetch image rate limits: ${response.statusText}`, |
|||
); |
|||
} |
|||
|
|||
return await response.json(); |
|||
} catch (error) { |
|||
logger.error("Error fetching image rate limits:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Formats rate limit error messages |
|||
* @param error - The error object |
|||
* @returns string |
|||
*/ |
|||
static formatRateLimitError(error: unknown): string { |
|||
if (error instanceof Error) { |
|||
return error.message; |
|||
} |
|||
if (typeof error === "object" && error !== null) { |
|||
const err = error as { |
|||
response?: { data?: { error?: { message?: string } } }; |
|||
}; |
|||
return err.response?.data?.error?.message || "An unknown error occurred"; |
|||
} |
|||
return "An unknown error occurred"; |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
/** |
|||
* @file DatabaseBackupService.ts |
|||
* @description Capacitor-specific implementation of the DatabaseBackupService |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { DatabaseBackupService } from "../../DatabaseBackupService"; |
|||
import { Filesystem } from "@capacitor/filesystem"; |
|||
import { Share } from "@capacitor/share"; |
|||
|
|||
export default class CapacitorDatabaseBackupService extends DatabaseBackupService { |
|||
protected async handleBackup( |
|||
base64Data: string, |
|||
_arrayBuffer: ArrayBuffer, |
|||
_blob: Blob, |
|||
): Promise<void> { |
|||
// Capacitor platform handling
|
|||
const fileName = `database-backup-${new Date().toISOString()}.json`; |
|||
const path = `backups/${fileName}`; |
|||
|
|||
await Filesystem.writeFile({ |
|||
path, |
|||
data: base64Data, |
|||
directory: "CACHE", |
|||
recursive: true, |
|||
}); |
|||
|
|||
await Share.share({ |
|||
title: "Database Backup", |
|||
text: "Here's your database backup", |
|||
url: path, |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
import { DatabaseBackupService } from "../../DatabaseBackupService"; |
|||
import { dialog } from "electron"; |
|||
import * as fs from "fs"; |
|||
import * as path from "path"; |
|||
|
|||
export default class ElectronDatabaseBackupService extends DatabaseBackupService { |
|||
protected async handleBackup(base64Data: string): Promise<void> { |
|||
const { filePath } = await dialog.showSaveDialog({ |
|||
title: "Save Database Backup", |
|||
defaultPath: path.join(process.env.HOME || "", "database-backup.json"), |
|||
filters: [{ name: "JSON", extensions: ["json"] }], |
|||
}); |
|||
|
|||
if (filePath) { |
|||
fs.writeFileSync(filePath, base64Data, "base64"); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
/** |
|||
* @file empty.ts |
|||
* @description Stub implementation for excluding platform-specific code |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { DatabaseBackupService } from "../DatabaseBackupService"; |
|||
|
|||
export default class StubDatabaseBackupService extends DatabaseBackupService { |
|||
protected async handleBackup( |
|||
_base64Data: string, |
|||
_arrayBuffer: ArrayBuffer, |
|||
_blob: Blob, |
|||
): Promise<void> { |
|||
throw new Error("This platform does not support database backups"); |
|||
} |
|||
} |
|||
|
|||
export { StubDatabaseBackupService as DatabaseBackupService }; |
@ -0,0 +1,33 @@ |
|||
/** |
|||
* @file DatabaseBackupService.ts |
|||
* @description Web-specific implementation of the DatabaseBackupService |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { DatabaseBackupService } from "../../DatabaseBackupService"; |
|||
import { log, error } from "../../../utils/logger"; |
|||
|
|||
export default class WebDatabaseBackupService extends DatabaseBackupService { |
|||
protected async handleBackup( |
|||
_base64Data: string, |
|||
_arrayBuffer: ArrayBuffer, |
|||
blob: Blob, |
|||
): Promise<void> { |
|||
try { |
|||
log("Starting web platform backup"); |
|||
const url = URL.createObjectURL(blob); |
|||
const a = document.createElement("a"); |
|||
a.href = url; |
|||
a.download = `database-backup-${new Date().toISOString()}.json`; |
|||
document.body.appendChild(a); |
|||
a.click(); |
|||
document.body.removeChild(a); |
|||
URL.revokeObjectURL(url); |
|||
log("Web platform backup completed"); |
|||
} catch (err) { |
|||
error("Error during web platform backup:", err); |
|||
throw err; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,64 @@ |
|||
/** |
|||
* Type declarations for Capacitor modules used in the application. |
|||
* @author Matthew Raymer |
|||
*/ |
|||
|
|||
declare module "@capacitor/filesystem" { |
|||
export interface FileWriteOptions { |
|||
path: string; |
|||
data: string; |
|||
directory?: string; |
|||
encoding?: string; |
|||
recursive?: boolean; |
|||
} |
|||
|
|||
export interface FileReadResult { |
|||
data: string; |
|||
} |
|||
|
|||
export interface FileDeleteOptions { |
|||
path: string; |
|||
directory?: string; |
|||
} |
|||
|
|||
export interface FilesystemDirectory { |
|||
Cache: "CACHE"; |
|||
Documents: "DOCUMENTS"; |
|||
Data: "DATA"; |
|||
External: "EXTERNAL"; |
|||
ExternalStorage: "EXTERNAL_STORAGE"; |
|||
} |
|||
|
|||
export interface Filesystem { |
|||
writeFile(options: FileWriteOptions): Promise<void>; |
|||
readFile(options: { |
|||
path: string; |
|||
directory?: string; |
|||
}): Promise<FileReadResult>; |
|||
deleteFile(options: FileDeleteOptions): Promise<void>; |
|||
} |
|||
|
|||
export const Filesystem: Filesystem; |
|||
export const Directory: FilesystemDirectory; |
|||
export const Encoding: { |
|||
UTF8: "utf8"; |
|||
ASCII: "ascii"; |
|||
UTF16: "utf16"; |
|||
}; |
|||
} |
|||
|
|||
declare module "@capacitor/share" { |
|||
export interface ShareOptions { |
|||
title?: string; |
|||
text?: string; |
|||
url?: string; |
|||
dialogTitle?: string; |
|||
files?: string[]; |
|||
} |
|||
|
|||
export interface Share { |
|||
share(options: ShareOptions): Promise<void>; |
|||
} |
|||
|
|||
export const Share: Share; |
|||
} |
@ -0,0 +1,430 @@ |
|||
/** |
|||
* @file interfaces.ts |
|||
* @description Core type declarations for the TimeSafari application |
|||
* |
|||
* This module defines the core interfaces and types used throughout the application. |
|||
* It serves as the central location for type definitions that are shared across |
|||
* multiple components and services. |
|||
* |
|||
* Architecture: |
|||
* 1. DID (Decentralized Identifier) Types: |
|||
* - IIdentifier: Core DID structure |
|||
* - IKey: Cryptographic key information |
|||
* - IService: Service endpoints and capabilities |
|||
* |
|||
* 2. Verifiable Credential Types: |
|||
* - GenericCredWrapper: Base wrapper for all credentials |
|||
* - GiveVerifiableCredential: Gift-related credentials |
|||
* - OfferVerifiableCredential: Offer-related credentials |
|||
* - RegisterVerifiableCredential: Registration credentials |
|||
* |
|||
* 3. Service Types: |
|||
* - EndorserService: Claims and endorsements |
|||
* - PushNotificationService: Web push notifications |
|||
* - ProfileService: User profiles |
|||
* - BackupService: Data backup |
|||
* |
|||
* @see src/interfaces/identifier.ts |
|||
* @see src/interfaces/claims.ts |
|||
* @see src/interfaces/limits.ts |
|||
*/ |
|||
|
|||
import { GiveVerifiableCredential } from "../interfaces"; |
|||
|
|||
/** |
|||
* Interface for a Decentralized Identifier (DID) |
|||
* |
|||
* This interface defines the structure of a DID, which is a unique identifier |
|||
* that can be used to look up a DID document containing information associated |
|||
* with the DID, such as public keys and service endpoints. |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const identifier: IIdentifier = { |
|||
* did: 'did:ethr:0x123...', |
|||
* provider: 'ethr', |
|||
* keys: [{ |
|||
* kid: 'keys-1', |
|||
* kms: 'local', |
|||
* type: 'Secp256k1', |
|||
* publicKeyHex: '0x...', |
|||
* meta: { derivationPath: "m/44'/60'/0'/0/0" } |
|||
* }], |
|||
* services: [{ |
|||
* id: 'endorser-service', |
|||
* type: 'EndorserService', |
|||
* serviceEndpoint: 'https://api.endorser.ch' |
|||
* }] |
|||
* }; |
|||
* ``` |
|||
*/ |
|||
export interface IIdentifier { |
|||
/** |
|||
* The DID string in the format 'did:method:identifier' |
|||
* @example 'did:ethr:0x123...' |
|||
*/ |
|||
did: string; |
|||
|
|||
/** |
|||
* The DID method provider |
|||
* @example 'ethr' |
|||
*/ |
|||
provider: string; |
|||
|
|||
/** |
|||
* Array of cryptographic keys associated with the DID |
|||
*/ |
|||
keys: Array<{ |
|||
/** |
|||
* Key identifier |
|||
* @example 'keys-1' |
|||
*/ |
|||
kid: string; |
|||
|
|||
/** |
|||
* Key management system |
|||
* @example 'local' |
|||
*/ |
|||
kms: string; |
|||
|
|||
/** |
|||
* Key type |
|||
* @example 'Secp256k1' |
|||
*/ |
|||
type: string; |
|||
|
|||
/** |
|||
* Public key in hexadecimal format |
|||
* @example '0x...' |
|||
*/ |
|||
publicKeyHex: string; |
|||
|
|||
/** |
|||
* Optional metadata about the key |
|||
*/ |
|||
meta?: { |
|||
/** |
|||
* HD wallet derivation path |
|||
* @example "m/44'/60'/0'/0/0" |
|||
*/ |
|||
derivationPath?: string; |
|||
|
|||
/** |
|||
* Key usage or purpose |
|||
* @example "signing", "encryption" |
|||
*/ |
|||
usage?: string; |
|||
|
|||
/** |
|||
* Key creation timestamp |
|||
*/ |
|||
createdAt?: number; |
|||
|
|||
/** |
|||
* Additional key metadata |
|||
*/ |
|||
[key: string]: unknown; |
|||
}; |
|||
}>; |
|||
|
|||
/** |
|||
* Array of service endpoints associated with the DID |
|||
*/ |
|||
services: Array<{ |
|||
/** |
|||
* Service identifier |
|||
* @example 'endorser-service' |
|||
*/ |
|||
id: string; |
|||
|
|||
/** |
|||
* Service type |
|||
* @example 'EndorserService' |
|||
*/ |
|||
type: string; |
|||
|
|||
/** |
|||
* Service endpoint URL |
|||
* @example 'https://api.endorser.ch' |
|||
*/ |
|||
serviceEndpoint: string; |
|||
|
|||
/** |
|||
* Optional service description |
|||
*/ |
|||
description?: string; |
|||
}>; |
|||
|
|||
/** |
|||
* Optional metadata about the identifier |
|||
*/ |
|||
meta?: { |
|||
/** |
|||
* DID method-specific metadata |
|||
* @example { network: "mainnet", chainId: 1 } for ethr |
|||
*/ |
|||
method?: Record<string, unknown>; |
|||
|
|||
/** |
|||
* Identifier creation timestamp |
|||
*/ |
|||
createdAt?: number; |
|||
|
|||
/** |
|||
* Last update timestamp |
|||
*/ |
|||
updatedAt?: number; |
|||
|
|||
/** |
|||
* Additional identifier metadata |
|||
*/ |
|||
[key: string]: unknown; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Interface for a cryptographic key |
|||
* |
|||
* This interface defines the structure of a cryptographic key used in the |
|||
* DID system. It includes both public and private key information, along |
|||
* with metadata about the key's purpose and derivation. |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const key: IKey = { |
|||
* id: 'did:ethr:0x123...#keys-1', |
|||
* type: 'Secp256k1VerificationKey2018', |
|||
* controller: 'did:ethr:0x123...', |
|||
* ethereumAddress: '0x123...', |
|||
* publicKeyHex: '0x...', |
|||
* privateKeyHex: '0x...', |
|||
* meta: { |
|||
* derivationPath: "m/44'/60'/0'/0/0" |
|||
* } |
|||
* }; |
|||
* ``` |
|||
*/ |
|||
export interface IKey { |
|||
/** |
|||
* Unique identifier for the key |
|||
* @example 'did:ethr:0x123...#keys-1' |
|||
*/ |
|||
id: string; |
|||
|
|||
/** |
|||
* Key type specification |
|||
* @example 'Secp256k1VerificationKey2018' |
|||
*/ |
|||
type: string; |
|||
|
|||
/** |
|||
* DID that controls this key |
|||
* @example 'did:ethr:0x123...' |
|||
*/ |
|||
controller: string; |
|||
|
|||
/** |
|||
* Associated Ethereum address |
|||
* @example '0x123...' |
|||
*/ |
|||
ethereumAddress: string; |
|||
|
|||
/** |
|||
* Public key in hexadecimal format |
|||
* @example '0x...' |
|||
*/ |
|||
publicKeyHex: string; |
|||
|
|||
/** |
|||
* Private key in hexadecimal format |
|||
* @example '0x...' |
|||
*/ |
|||
privateKeyHex: string; |
|||
|
|||
/** |
|||
* Optional metadata about the key |
|||
*/ |
|||
meta?: { |
|||
/** |
|||
* HD wallet derivation path |
|||
* @example "m/44'/60'/0'/0/0" |
|||
*/ |
|||
derivationPath?: string; |
|||
|
|||
/** |
|||
* Key usage or purpose |
|||
* @example "signing", "encryption" |
|||
*/ |
|||
usage?: string; |
|||
|
|||
/** |
|||
* Key creation timestamp |
|||
*/ |
|||
createdAt?: number; |
|||
|
|||
/** |
|||
* Additional key metadata |
|||
*/ |
|||
[key: string]: unknown; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Interface for a service endpoint |
|||
* |
|||
* This interface defines the structure of a service endpoint that can be |
|||
* associated with a DID. Services provide additional functionality and |
|||
* endpoints for DID operations. |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* const service: IService = { |
|||
* id: 'endorser-service', |
|||
* type: 'EndorserService', |
|||
* serviceEndpoint: 'https://api.endorser.ch', |
|||
* description: 'Service for handling claims and endorsements', |
|||
* metadata: { |
|||
* version: '1.0.0', |
|||
* capabilities: ['claims', 'endorsements'], |
|||
* config: { |
|||
* apiServer: 'https://api.endorser.ch' |
|||
* } |
|||
* } |
|||
* }; |
|||
* ``` |
|||
*/ |
|||
export interface IService { |
|||
/** |
|||
* Unique identifier for the service |
|||
* @example 'endorser-service' |
|||
*/ |
|||
id: string; |
|||
|
|||
/** |
|||
* Type of service |
|||
* @example 'EndorserService' |
|||
*/ |
|||
type: string; |
|||
|
|||
/** |
|||
* Service endpoint URL |
|||
* @example 'https://api.endorser.ch' |
|||
*/ |
|||
serviceEndpoint: string; |
|||
|
|||
/** |
|||
* Optional human-readable description |
|||
*/ |
|||
description?: string; |
|||
|
|||
/** |
|||
* Optional service metadata |
|||
*/ |
|||
metadata?: { |
|||
/** |
|||
* Service version |
|||
* @example '1.0.0' |
|||
*/ |
|||
version?: string; |
|||
|
|||
/** |
|||
* Array of service capabilities |
|||
* @example ['claims', 'endorsements'] |
|||
*/ |
|||
capabilities?: string[]; |
|||
|
|||
/** |
|||
* Service-specific configuration |
|||
*/ |
|||
config?: Record<string, unknown>; |
|||
}; |
|||
} |
|||
|
|||
export interface ExportProgress { |
|||
status: "preparing" | "exporting" | "complete" | "error"; |
|||
message?: string; |
|||
error?: Error; |
|||
} |
|||
|
|||
export interface UserProfile { |
|||
/** User's profile description */ |
|||
description: string; |
|||
/** User's location information */ |
|||
location?: { |
|||
/** Latitude coordinate */ |
|||
lat: number; |
|||
/** Longitude coordinate */ |
|||
lng: number; |
|||
}; |
|||
/** User's given name */ |
|||
givenName?: string; |
|||
/** User's family name */ |
|||
familyName?: string; |
|||
} |
|||
|
|||
export interface ErrorResponse { |
|||
error: string; |
|||
message: string; |
|||
statusCode: number; |
|||
} |
|||
|
|||
export interface LeafletMouseEvent { |
|||
latlng: { |
|||
lat: number; |
|||
lng: number; |
|||
}; |
|||
} |
|||
|
|||
export interface GiveRecordWithContactInfo { |
|||
type?: string; |
|||
agentDid: string; |
|||
amount: number; |
|||
amountConfirmed: number; |
|||
description: string; |
|||
fullClaim: GiveVerifiableCredential; |
|||
fulfillsHandleId: string; |
|||
fulfillsPlanHandleId?: string; |
|||
fulfillsType?: string; |
|||
handleId: string; |
|||
issuedAt: string; |
|||
issuerDid: string; |
|||
jwtId: string; |
|||
providerPlanHandleId?: string; |
|||
recipientDid: string; |
|||
unit: string; |
|||
giver: { |
|||
known: boolean; |
|||
displayName: string; |
|||
profileImageUrl?: string; |
|||
}; |
|||
issuer: { |
|||
known: boolean; |
|||
displayName: string; |
|||
profileImageUrl?: string; |
|||
}; |
|||
receiver: { |
|||
known: boolean; |
|||
displayName: string; |
|||
profileImageUrl?: string; |
|||
}; |
|||
providerPlanName?: string; |
|||
recipientProjectName?: string; |
|||
image?: string; |
|||
} |
|||
|
|||
export interface TimeSafariError extends Error { |
|||
/** |
|||
* User-friendly error message |
|||
*/ |
|||
userMessage?: string; |
|||
|
|||
/** |
|||
* Error code for programmatic handling |
|||
*/ |
|||
code?: string; |
|||
|
|||
/** |
|||
* Additional error context |
|||
*/ |
|||
context?: Record<string, unknown>; |
|||
} |
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,46 @@ |
|||
import { defineConfig } from "vite"; |
|||
import vue from "@vitejs/plugin-vue"; |
|||
import path from "path"; |
|||
|
|||
export default defineConfig({ |
|||
plugins: [vue()], |
|||
resolve: { |
|||
alias: { |
|||
'@': path.resolve(__dirname, './src'), |
|||
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), |
|||
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'), |
|||
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), |
|||
stream: 'stream-browserify', |
|||
util: 'util', |
|||
crypto: 'crypto-browserify' |
|||
}, |
|||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], |
|||
}, |
|||
optimizeDeps: { |
|||
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'], |
|||
esbuildOptions: { |
|||
define: { |
|||
global: 'globalThis' |
|||
} |
|||
} |
|||
}, |
|||
build: { |
|||
sourcemap: true, |
|||
target: 'esnext', |
|||
chunkSizeWarningLimit: 1000, |
|||
commonjsOptions: { |
|||
include: [/node_modules/], |
|||
transformMixedEsModules: true |
|||
}, |
|||
rollupOptions: { |
|||
external: ['stream', 'util', 'crypto'], |
|||
output: { |
|||
globals: { |
|||
stream: 'stream', |
|||
util: 'util', |
|||
crypto: 'crypto' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}); |
File diff suppressed because one or more lines are too long
@ -0,0 +1,28 @@ |
|||
import { defineConfig, loadEnv } from "vite"; |
|||
import baseConfig from "./vite.config.base"; |
|||
|
|||
export default defineConfig(({ mode }) => { |
|||
const env = loadEnv(mode, process.cwd(), ''); |
|||
|
|||
return { |
|||
...baseConfig, |
|||
define: { |
|||
'import.meta.env.VITE_PLATFORM': JSON.stringify('mobile'), |
|||
}, |
|||
build: { |
|||
...baseConfig.build, |
|||
outDir: 'dist/mobile', |
|||
rollupOptions: { |
|||
...baseConfig.build.rollupOptions, |
|||
output: { |
|||
...baseConfig.build.rollupOptions.output, |
|||
manualChunks: { |
|||
// Mobile-specific chunk splitting
|
|||
vendor: ['vue', 'vue-router', 'pinia'], |
|||
capacitor: ['@capacitor/core', '@capacitor/filesystem', '@capacitor/share'], |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
}); |
@ -1,46 +1,18 @@ |
|||
import { defineConfig } from "vite"; |
|||
import vue from "@vitejs/plugin-vue"; |
|||
import path from "path"; |
|||
import { defineConfig, loadEnv } from "vite"; |
|||
import baseConfig from "./vite.config.base"; |
|||
|
|||
export default defineConfig({ |
|||
plugins: [vue()], |
|||
resolve: { |
|||
alias: { |
|||
'@': path.resolve(__dirname, './src'), |
|||
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), |
|||
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'), |
|||
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), |
|||
stream: 'stream-browserify', |
|||
util: 'util', |
|||
crypto: 'crypto-browserify' |
|||
}, |
|||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], |
|||
}, |
|||
optimizeDeps: { |
|||
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'], |
|||
esbuildOptions: { |
|||
define: { |
|||
global: 'globalThis' |
|||
} |
|||
} |
|||
}, |
|||
build: { |
|||
sourcemap: true, |
|||
target: 'esnext', |
|||
chunkSizeWarningLimit: 1000, |
|||
commonjsOptions: { |
|||
include: [/node_modules/], |
|||
transformMixedEsModules: true |
|||
}, |
|||
rollupOptions: { |
|||
external: ['stream', 'util', 'crypto'], |
|||
output: { |
|||
globals: { |
|||
stream: 'stream', |
|||
util: 'util', |
|||
crypto: 'crypto' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
// https://vitejs.dev/config/
|
|||
export default defineConfig(({ mode }) => { |
|||
// Load env file based on `mode` in the current working directory.
|
|||
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
|
|||
const env = loadEnv(mode, process.cwd(), ''); |
|||
const platform = env.PLATFORM || 'web'; |
|||
|
|||
// Load platform-specific config
|
|||
const platformConfig = require(`./vite.config.${platform}`).default; |
|||
|
|||
return { |
|||
...baseConfig, |
|||
...platformConfig, |
|||
}; |
|||
}); |
@ -1,27 +1,92 @@ |
|||
import { defineConfig, mergeConfig } from "vite"; |
|||
import { VitePWA } from "vite-plugin-pwa"; |
|||
import { createBuildConfig } from "./vite.config.common.mts"; |
|||
import { loadAppConfig } from "./vite.config.utils.mts"; |
|||
/** |
|||
* @file vite.config.web.mts |
|||
* @description Vite configuration for web platform builds |
|||
* |
|||
* This configuration file defines how the application is built for web platforms. |
|||
* It extends the base configuration with web-specific settings and optimizations. |
|||
* |
|||
* Build Process Integration: |
|||
* 1. Configuration Loading: |
|||
* - Loads environment variables based on build mode |
|||
* - Merges base configuration from vite.config.common.mts |
|||
* - Loads application-specific configuration |
|||
* |
|||
* 2. Platform Definition: |
|||
* - Sets VITE_PLATFORM environment variable to 'web' |
|||
* - Used by PlatformServiceFactory to load web-specific implementations |
|||
* |
|||
* 3. Build Output: |
|||
* - Outputs to 'dist/web' directory |
|||
* - Creates vendor chunk for Vue-related dependencies |
|||
* - Enables PWA features with auto-update capability |
|||
* |
|||
* 4. Development vs Production: |
|||
* - Development: Enables source maps and development features |
|||
* - Production: Optimizes chunks and enables PWA features |
|||
* |
|||
* Usage: |
|||
* - Development: npm run dev |
|||
* - Production: npm run build:web |
|||
* |
|||
* @see vite.config.common.mts |
|||
* @see vite.config.utils.mts |
|||
* @see PlatformServiceFactory.ts |
|||
*/ |
|||
|
|||
export default defineConfig(async () => { |
|||
const baseConfig = await createBuildConfig('web'); |
|||
const appConfig = await loadAppConfig(); |
|||
import { defineConfig } from "vite"; |
|||
import vue from "@vitejs/plugin-vue"; |
|||
import baseConfig from "./vite.config.base"; |
|||
|
|||
return mergeConfig(baseConfig, { |
|||
plugins: [ |
|||
VitePWA({ |
|||
registerType: 'autoUpdate', |
|||
manifest: appConfig.pwaConfig?.manifest, |
|||
devOptions: { |
|||
enabled: false |
|||
}, |
|||
workbox: { |
|||
cleanupOutdatedCaches: true, |
|||
skipWaiting: true, |
|||
clientsClaim: true, |
|||
sourcemap: true |
|||
} |
|||
}) |
|||
] |
|||
}); |
|||
// Define Node.js built-in modules that need browser compatibility |
|||
const nodeBuiltins = { |
|||
stream: 'stream-browserify', |
|||
util: 'util', |
|||
crypto: 'crypto-browserify', |
|||
http: 'stream-http', |
|||
https: 'https-browserify', |
|||
zlib: 'browserify-zlib', |
|||
url: 'url', |
|||
assert: 'assert', |
|||
path: 'path-browserify', |
|||
fs: 'browserify-fs', |
|||
tty: 'tty-browserify' |
|||
}; |
|||
|
|||
export default defineConfig({ |
|||
...baseConfig, |
|||
plugins: [vue()], |
|||
optimizeDeps: { |
|||
...baseConfig.optimizeDeps, |
|||
include: [...(baseConfig.optimizeDeps?.include || []), 'qrcode.vue'], |
|||
exclude: Object.keys(nodeBuiltins), |
|||
esbuildOptions: { |
|||
define: { |
|||
global: 'globalThis' |
|||
} |
|||
} |
|||
}, |
|||
resolve: { |
|||
...baseConfig.resolve, |
|||
alias: { |
|||
...baseConfig.resolve?.alias, |
|||
...nodeBuiltins |
|||
} |
|||
}, |
|||
build: { |
|||
...baseConfig.build, |
|||
commonjsOptions: { |
|||
...baseConfig.build?.commonjsOptions, |
|||
include: [/node_modules/], |
|||
exclude: [/src\/services\/platforms\/electron/], |
|||
transformMixedEsModules: true |
|||
}, |
|||
rollupOptions: { |
|||
...baseConfig.build?.rollupOptions, |
|||
external: Object.keys(nodeBuiltins), |
|||
output: { |
|||
...baseConfig.build?.rollupOptions?.output, |
|||
globals: nodeBuiltins |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
|
Loading…
Reference in new issue