Merge changes

This commit is contained in:
Matthew Raymer
2025-02-03 13:27:36 +00:00
24 changed files with 28864 additions and 27541 deletions

View File

@@ -77,7 +77,9 @@
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
If you want to be contacted, be sure to include your contact information.
If you want to be contacted, be sure to include your contact information
-- just remember that this information is public and saved in a public
history.
</div>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ fullClaim.description?.length }}/5000 max. characters
@@ -152,11 +154,17 @@
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
<label>Send to Trustroots</label>
<fa
icon="circle-info"
class="text-blue-500 ml-2 cursor-pointer"
@click.stop="showNostrPartnerInfo"
/>
</div>
<!--
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
<label>Send to TripHopping</label>
<fa icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
</div>
-->
</div>
@@ -195,8 +203,12 @@ import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { hexToBytes } from "@noble/hashes/utils";
import type { EventTemplate, VerifiedEvent } from "nostr-tools/lib/types/core";
import { accountFromSeedWords } from "nostr-tools/nip06";
// these core imports could also be included as "import type ..."
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
import {
accountFromExtendedKey,
extendedKeysFromSeedWords,
} from "nostr-tools/nip06";
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
@@ -217,7 +229,6 @@ import {
} from "../libs/endorserServer";
import {
retrieveAccountCount,
retrieveAccountMetadata,
retrieveFullyDecryptedAccount,
} from "../libs/util";
@@ -408,13 +419,26 @@ export default class NewEditProjectView extends Vue {
delete vcClaim.image;
}
if (this.includeLocation) {
vcClaim.location = {
geo: {
"@type": "GeoCoordinates",
latitude: this.latitude,
longitude: this.longitude,
},
};
if (!this.latitude || !this.longitude) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Location Error",
text: "The location was invalid so it was not set.",
},
5000,
);
delete vcClaim.location;
} else {
vcClaim.location = {
geo: {
"@type": "GeoCoordinates",
latitude: this.latitude,
longitude: this.longitude,
},
};
}
} else {
delete vcClaim.location;
}
@@ -431,7 +455,7 @@ export default class NewEditProjectView extends Vue {
{
group: "alert",
type: "danger",
title: "Error",
title: "Date Error",
text: "The date was invalid so it was not set.",
},
5000,
@@ -451,30 +475,58 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) {
this.$notify(
{
group: "alert",
type: "success",
title: "Saved",
text: "The project was saved successfully.",
},
3000,
);
this.errorMessage = "";
const projectPath = encodeURIComponent(resp.data.success.handleId);
let signedPayload: VerifiedEvent | undefined; // sign something to prove ownership of pubkey
if (this.sendToTrustroots) {
signedPayload = await this.signPayload();
this.sendToNostrPartner(
"NOSTR-EVENT-TRUSTROOTS",
"Trustroots",
resp.data.success.claimId,
signedPayload,
);
}
if (this.sendToTripHopping) {
if (!signedPayload) {
signedPayload = await this.signPayload();
if (this.sendToTrustroots || this.sendToTripHopping) {
if (this.latitude && this.longitude) {
let payloadAndKey; // sign something to prove ownership of pubkey
if (this.sendToTrustroots) {
payloadAndKey = await this.signSomePayload();
// not going to await... the save was successful, so we'll continue to the next page
this.sendToNostrPartner(
"NOSTR-EVENT-TRUSTROOTS",
"Trustroots",
resp.data.success.claimId,
payloadAndKey.signedEvent,
payloadAndKey.publicExtendedKey,
);
}
if (this.sendToTripHopping) {
if (!payloadAndKey) {
payloadAndKey = await this.signSomePayload();
}
// not going to await... the save was successful, so we'll continue to the next page
this.sendToNostrPartner(
"NOSTR-EVENT-TRIPHOPPING",
"TripHopping",
resp.data.success.claimId,
payloadAndKey.signedEvent,
payloadAndKey.publicExtendedKey,
);
}
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Partner Error",
text: "A partner was selected but the location was not set, so it was not sent to any partner.",
},
5000,
);
}
this.sendToNostrPartner(
"NOSTR-EVENT-TRIPHOPPING",
"TripHopping",
resp.data.success.claimId,
signedPayload,
);
}
(this.$router as Router).push({ path: "/project/" + projectPath });
@@ -541,19 +593,28 @@ export default class NewEditProjectView extends Vue {
}
}
private async signPayload(): Promise<VerifiedEvent> {
/**
* @return a signed payload and an extended public key for later transmission
*/
private async signSomePayload(): Promise<{
signedEvent: VerifiedEvent;
publicExtendedKey: string;
}> {
const account = await retrieveFullyDecryptedAccount(this.activeDid);
// get the last number of the derivationPath
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const pubPri = accountFromSeedWords(
const extPubPri = extendedKeysFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
);
const privateBytes = hexToBytes(pubPri?.privateKey);
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
const privateExtendedKey = extPubPri?.privateExtendedKey;
const privateKey = accountFromExtendedKey(privateExtendedKey).privateKey;
const privateBytes = hexToBytes(privateKey);
// No real content is necessary, we just want something signed,
// so we might as well use nostr libs for nostr functions.
// Besides: someday we may create real content that we can relay.
@@ -563,9 +624,12 @@ export default class NewEditProjectView extends Vue {
content: "",
created_at: 0,
};
// Why does IntelliJ not see matching types?
const signedEvent = finalizeEvent(event, privateBytes);
return signedEvent;
const signedEvent: VerifiedEvent = finalizeEvent(
// Why does IntelliJ not see matching types?
event as EventTemplate,
privateBytes,
) as VerifiedEvent;
return { signedEvent, publicExtendedKey };
}
private async sendToNostrPartner(
@@ -573,41 +637,37 @@ export default class NewEditProjectView extends Vue {
serviceName: string,
jwtId: string,
signedPayload: VerifiedEvent,
publicExtendedKey: string,
) {
// first, get the public key for nostr
const account = await retrieveAccountMetadata(this.activeDid);
// get the last number of the derivationPath
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const pubPri = accountFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
);
const nostrPubKey = pubPri?.publicKey;
let partnerServer = DEFAULT_PARTNER_API_SERVER;
const settings = await retrieveSettingsForActiveAccount();
if (settings.partnerApiServer) {
partnerServer = settings.partnerApiServer;
}
const endorserPartnerUrl = partnerServer + "/api/partner/link";
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
const content = this.fullClaim.name + " - see " + timeSafariUrl;
// Why does IntelliJ not see matching types?
const payload = serializeEvent(signedPayload);
const partnerParams = {
jwtId: jwtId,
linkCode: linkCode,
inputJson: JSON.stringify(content),
pubKeyHex: nostrPubKey,
pubKeyImage: payload,
pubKeySigHex: signedPayload.sig,
};
const headers = await getHeaders(this.activeDid);
try {
let partnerServer = DEFAULT_PARTNER_API_SERVER;
const settings = await retrieveSettingsForActiveAccount();
if (settings.partnerApiServer) {
partnerServer = settings.partnerApiServer;
}
const endorserPartnerUrl = partnerServer + "/api/partner/link";
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
const content = this.fullClaim.name + " - see " + timeSafariUrl;
const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
const unsignedPayload: UnsignedEvent = {
// why doesn't "...signedPayload" work?
kind: signedPayload.kind,
tags: signedPayload.tags,
content: signedPayload.content,
created_at: signedPayload.created_at,
pubkey: publicKeyHex,
};
// Why does IntelliJ not see matching types?
const payload = serializeEvent(unsignedPayload as UnsignedEvent);
const partnerParams = {
jwtId: jwtId,
linkCode: linkCode,
inputJson: JSON.stringify(content),
pubKeyHex: publicKeyHex,
pubKeyImage: payload,
pubKeySigHex: signedPayload.sig,
};
const headers = await getHeaders(this.activeDid);
const linkResp = await this.axios.post(
endorserPartnerUrl,
partnerParams,
@@ -689,5 +749,17 @@ export default class NewEditProjectView extends Vue {
public onCancelClick() {
(this.$router as Router).back();
}
public showNostrPartnerInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "About Nostr Events",
text: "This will submit this project to a partner on the nostr network. It will contain your public key data which may allow correlation, so don't allow this if you're not comfortable with that.",
},
7000,
);
}
}
</script>