mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(quartz): implement NIP-5D (Nostr Web Applets / napplets)
Adds the full NIP-5D napplet manifest layer, plus the NIP-5A aggregate-hash infrastructure it depends on. Follows the nip88Polls package structure (event class + tags/ + TagArrayExt + TagArrayBuilderExt). NIP-5A shared infra (nip5aStaticWebsites): - XTag — the aggregate-hash tag ["x", "<sha256>", "aggregate"]. - SiteAggregateHash — computes/verifies the NIP-5A aggregate hash: sort the per-path lines "<hash> <path>\n" lexicographically, concat as UTF-8, SHA-256. Pinned by a test against an independently computed sha256sum vector. - siteAggregateHash() parse + builder extensions. NIP-5D napplets (nip5dNapplets): - NappletSnapshotEvent (5129, regular), RootNappletEvent (15129, replaceable), NamedNappletEvent (35129, addressable, d-tag) — all built on the NIP-5A path/server/title/description/source/x tag set. - RequiresTag — ["requires", "<bare-nap-name>"] capability declarations. - NappletManifest interface — uniform accessors (paths/servers/requires/title/ …) plus computeAggregateHash()/verifyAggregate() shared across the three kinds. build() auto-stamps the x aggregate (required for snapshots, recommended for root/named). - Registered all three kinds in EventFactory. This covers the NIP-5D runtime verification contract end-to-end in quartz: signature (core Event.verify), per-blob sha256 (StaticSiteResolver.verify), and the aggregate x-tag (NappletManifest.verifyAggregate). Note the napplet kinds 5129/15129/35129 are distinct from the NIP-5A nsite kinds 5128/15128/35128, so there is no collision. Tests: SiteAggregateHashTest (vector + order-independence + tamper) and NappletEventTest (build/parse round-trip for all three kinds, aggregate verification, tamper detection, EventFactory routing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5aStaticWebsites
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
|
||||
/**
|
||||
* NIP-5A aggregate hash — a single content hash binding every file in a
|
||||
* static-website / napplet manifest, carried in the `["x", <hash>, "aggregate"]`
|
||||
* tag and verified by NIP-5D runtimes before executing a napplet.
|
||||
*
|
||||
* Algorithm (verbatim from NIP-5A):
|
||||
* 1. Collect every `path` tag.
|
||||
* 2. For each, produce a line `"<sha256hash> <absolute-path>\n"`.
|
||||
* 3. Sort all lines in ascending lexicographic order.
|
||||
* 4. Concatenate the sorted lines as UTF-8 bytes.
|
||||
* 5. SHA-256 the concatenation.
|
||||
*
|
||||
* Lines are sorted by Kotlin's natural `String` order (UTF-16 code units), which
|
||||
* matches a JavaScript `Array.prototype.sort()` reference implementation. In
|
||||
* practice each line is prefixed by its unique 64-char hex hash, so ordering is
|
||||
* decided by the hash and the path encoding only ever breaks ties between two
|
||||
* paths sharing one blob.
|
||||
*/
|
||||
object SiteAggregateHash {
|
||||
/** Recomputes the aggregate hash hex from a manifest's [paths]. */
|
||||
fun compute(paths: List<PathTag>): HexKey {
|
||||
val body =
|
||||
paths
|
||||
.map { "${it.hash} ${it.path}\n" }
|
||||
.sorted()
|
||||
.joinToString("")
|
||||
return sha256(body.encodeToByteArray()).toHexKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a declared aggregate hash against the one recomputed from [paths].
|
||||
* Returns `true` when [declared] is null (nothing to check) — per NIP-5D,
|
||||
* the `x` tag is only enforced when present.
|
||||
*/
|
||||
fun verify(
|
||||
paths: List<PathTag>,
|
||||
declared: HexKey?,
|
||||
): Boolean = declared == null || declared.equals(compute(paths), ignoreCase = true)
|
||||
}
|
||||
+7
@@ -21,12 +21,14 @@
|
||||
package com.vitorpamplona.quartz.nip5aStaticWebsites
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.DescriptionTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.ServerTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.SourceTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.XTag
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.sitePaths(paths: List<PathTag>) = addAll(PathTag.assemble(paths))
|
||||
|
||||
@@ -37,3 +39,8 @@ fun <T : Event> TagArrayBuilder<T>.siteTitle(title: String) = addUnique(TitleTag
|
||||
fun <T : Event> TagArrayBuilder<T>.siteDescription(description: String) = addUnique(DescriptionTag.assemble(description))
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.siteSource(url: String) = addUnique(SourceTag.assemble(url))
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.siteAggregateHash(aggregateHash: HexKey) = addUnique(XTag.assemble(aggregateHash))
|
||||
|
||||
/** Computes the NIP-5A aggregate hash from [paths] and adds it as the `x` tag. */
|
||||
fun <T : Event> TagArrayBuilder<T>.siteAggregateHash(paths: List<PathTag>) = siteAggregateHash(SiteAggregateHash.compute(paths))
|
||||
|
||||
+3
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.ServerTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.SourceTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.XTag
|
||||
|
||||
fun TagArray.sitePaths() = mapNotNull(PathTag::parse)
|
||||
|
||||
@@ -36,3 +37,5 @@ fun TagArray.siteTitle() = firstNotNullOfOrNull(TitleTag::parse)
|
||||
fun TagArray.siteDescription() = firstNotNullOfOrNull(DescriptionTag::parse)
|
||||
|
||||
fun TagArray.siteSource() = firstNotNullOfOrNull(SourceTag::parse)
|
||||
|
||||
fun TagArray.siteAggregateHash() = firstNotNullOfOrNull(XTag::parse)
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5aStaticWebsites.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* The NIP-5A aggregate-hash tag: `["x", "<sha256-hex>", "aggregate"]`.
|
||||
*
|
||||
* It pins the whole site/napplet manifest to a single content hash computed over
|
||||
* all `path` tags (see [com.vitorpamplona.quartz.nip5aStaticWebsites.SiteAggregateHash]).
|
||||
* The `"aggregate"` marker in position 2 distinguishes it from other `x` tags
|
||||
* (e.g. NIP-94's bare `["x", "<hash>"]`).
|
||||
*/
|
||||
class XTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "x"
|
||||
const val AGGREGATE_MARKER = "aggregate"
|
||||
|
||||
/** Returns the aggregate hash hex when [tag] is a well-formed aggregate `x` tag, else null. */
|
||||
fun parse(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[2] == AGGREGATE_MARKER) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(aggregateHash: HexKey) = arrayOf(TAG_NAME, aggregateHash, AGGREGATE_MARKER)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteAggregateHash
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteDescription
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.sitePaths
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteServers
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteSource
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteTitle
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* NIP-5D **named napplet** (kind 35129) — an addressable manifest identified by a
|
||||
* `d` tag, so one pubkey can publish many named napplets. Carries the NIP-5A tag
|
||||
* set; the `x` aggregate hash is recommended and included by [build].
|
||||
*/
|
||||
@Immutable
|
||||
class NamedNappletEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
NappletManifest,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = listOfNotNull(title(), description()).joinToString("\n")
|
||||
|
||||
fun identifier() = dTag()
|
||||
|
||||
companion object {
|
||||
const val KIND = 35129
|
||||
const val ALT_DESCRIPTION = "Named Napplet"
|
||||
|
||||
fun build(
|
||||
identifier: String,
|
||||
paths: List<PathTag>,
|
||||
servers: List<String> = emptyList(),
|
||||
requires: List<String> = emptyList(),
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
source: String? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<NamedNappletEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(identifier)
|
||||
sitePaths(paths)
|
||||
siteAggregateHash(paths)
|
||||
if (servers.isNotEmpty()) siteServers(servers)
|
||||
if (requires.isNotEmpty()) nappletRequires(requires)
|
||||
title?.let { siteTitle(it) }
|
||||
description?.let { siteDescription(it) }
|
||||
source?.let { siteSource(it) }
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.SiteAggregateHash
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteAggregateHash
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteDescription
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.sitePaths
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteServers
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteSource
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteTitle
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
|
||||
/**
|
||||
* Common surface for the three NIP-5D napplet manifest kinds — snapshot
|
||||
* ([NappletSnapshotEvent] / 5129), root ([RootNappletEvent] / 15129), and named
|
||||
* ([NamedNappletEvent] / 35129). They carry the same NIP-5A tag set and differ
|
||||
* only in replaceability and the `d` identifier, so all accessors live here and
|
||||
* read off the event's [tags].
|
||||
*/
|
||||
interface NappletManifest {
|
||||
val tags: TagArray
|
||||
|
||||
/** `path` tags mapping each absolute request path to its blob's sha256. */
|
||||
fun paths(): List<PathTag> = tags.sitePaths()
|
||||
|
||||
/** `server` tags hinting which Blossom servers hold the blobs. */
|
||||
fun servers(): List<String> = tags.siteServers()
|
||||
|
||||
/** `requires` tags: bare NAP capability domains the napplet needs from the shell. */
|
||||
fun requires(): List<String> = tags.nappletRequires()
|
||||
|
||||
/** The aggregate hash declared in the `x` tag, or null when absent. */
|
||||
fun declaredAggregateHash(): HexKey? = tags.siteAggregateHash()
|
||||
|
||||
fun title(): String? = tags.siteTitle()
|
||||
|
||||
fun description(): String? = tags.siteDescription()
|
||||
|
||||
fun source(): String? = tags.siteSource()
|
||||
|
||||
/** The NIP-5A aggregate hash recomputed from this manifest's [paths]. */
|
||||
fun computeAggregateHash(): HexKey = SiteAggregateHash.compute(paths())
|
||||
|
||||
/**
|
||||
* Verifies the declared `x` aggregate hash against the one recomputed from the
|
||||
* `path` tags. Returns `true` when no `x` tag is present — per NIP-5D it is only
|
||||
* enforced when carried. A runtime MUST still verify each blob's own sha256
|
||||
* separately (see `StaticSiteResolver`).
|
||||
*/
|
||||
fun verifyAggregate(): Boolean = SiteAggregateHash.verify(paths(), declaredAggregateHash())
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteAggregateHash
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteDescription
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.sitePaths
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteServers
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteSource
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteTitle
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* NIP-5D napplet **snapshot** (kind 5129) — a regular, immutable event that pins
|
||||
* one exact napplet build. Unlike the replaceable root/named manifests, a snapshot
|
||||
* is permanent version history; it MUST carry the `x` aggregate hash.
|
||||
*/
|
||||
@Immutable
|
||||
class NappletSnapshotEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
NappletManifest,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = listOfNotNull(title(), description()).joinToString("\n")
|
||||
|
||||
companion object {
|
||||
const val KIND = 5129
|
||||
const val ALT_DESCRIPTION = "Napplet snapshot"
|
||||
|
||||
fun build(
|
||||
paths: List<PathTag>,
|
||||
servers: List<String> = emptyList(),
|
||||
requires: List<String> = emptyList(),
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
source: String? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<NappletSnapshotEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
sitePaths(paths)
|
||||
siteAggregateHash(paths)
|
||||
if (servers.isNotEmpty()) siteServers(servers)
|
||||
if (requires.isNotEmpty()) nappletRequires(requires)
|
||||
title?.let { siteTitle(it) }
|
||||
description?.let { siteDescription(it) }
|
||||
source?.let { siteSource(it) }
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteAggregateHash
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteDescription
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.sitePaths
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteServers
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteSource
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.siteTitle
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* NIP-5D **root napplet** (kind 15129) — the replaceable manifest for a pubkey's
|
||||
* default napplet. One per author; newer events replace older ones. Carries the
|
||||
* NIP-5A tag set; the `x` aggregate hash is recommended and included by [build].
|
||||
*/
|
||||
@Immutable
|
||||
class RootNappletEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
NappletManifest,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = listOfNotNull(title(), description()).joinToString("\n")
|
||||
|
||||
companion object {
|
||||
const val KIND = 15129
|
||||
const val ALT_DESCRIPTION = "Napplet"
|
||||
|
||||
fun build(
|
||||
paths: List<PathTag>,
|
||||
servers: List<String> = emptyList(),
|
||||
requires: List<String> = emptyList(),
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
source: String? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<RootNappletEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
sitePaths(paths)
|
||||
siteAggregateHash(paths)
|
||||
if (servers.isNotEmpty()) siteServers(servers)
|
||||
if (requires.isNotEmpty()) nappletRequires(requires)
|
||||
title?.let { siteTitle(it) }
|
||||
description?.let { siteDescription(it) }
|
||||
source?.let { siteSource(it) }
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.tags.RequiresTag
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.nappletRequires(napNames: List<String>) = addAll(RequiresTag.assemble(napNames))
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.tags.RequiresTag
|
||||
|
||||
fun TagArray.nappletRequires() = mapNotNull(RequiresTag::parse)
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* NIP-5D capability-requirement tag: `["requires", "<nap-name>"]`.
|
||||
*
|
||||
* Each value is a bare NAP domain the napplet needs from its shell — e.g.
|
||||
* `relay`, `identity`, `storage` — never the `NAP-RELAY` spec name. A shell uses
|
||||
* these to decide which capabilities to broker (and which to deny) before running
|
||||
* the napplet.
|
||||
*/
|
||||
class RequiresTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "requires"
|
||||
|
||||
fun parse(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(napName: String) = arrayOf(TAG_NAME, napName)
|
||||
|
||||
fun assemble(napNames: List<String>) = napNames.map { assemble(it) }
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,9 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.NappletSnapshotEvent
|
||||
import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.quote.CashuMintQuoteEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent
|
||||
@@ -498,6 +501,9 @@ class EventFactory {
|
||||
TokenListEvent.KIND -> TokenListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
TokenRemovalEvent.KIND -> TokenRemovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NamedSiteEvent.KIND -> NamedSiteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NappletSnapshotEvent.KIND -> NappletSnapshotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RootNappletEvent.KIND -> RootNappletEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NamedNappletEvent.KIND -> NamedNappletEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NipTextEvent.KIND -> NipTextEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NutzapEvent.KIND -> NutzapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5aStaticWebsites
|
||||
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SiteAggregateHashTest {
|
||||
private val h1 = "11".repeat(32)
|
||||
private val h2 = "22".repeat(32)
|
||||
|
||||
@Test
|
||||
fun matchesIndependentlyComputedVector() {
|
||||
// Pinned with `printf '<h1> /index.html\n<h2> /app.js\n' | sha256sum` (h1 < h2, so sorted order).
|
||||
val paths = listOf(PathTag("/index.html", h1), PathTag("/app.js", h2))
|
||||
assertEquals(
|
||||
"2c1250d51fba528f4d8c3c98522ecd844f6dcd94bcf3ecc90f3219bbc4a23224",
|
||||
SiteAggregateHash.compute(paths),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isIndependentOfInputOrder() {
|
||||
val forward = SiteAggregateHash.compute(listOf(PathTag("/index.html", h1), PathTag("/app.js", h2)))
|
||||
val reversed = SiteAggregateHash.compute(listOf(PathTag("/app.js", h2), PathTag("/index.html", h1)))
|
||||
assertEquals(forward, reversed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyAcceptsNullAndMatchAndRejectsTamper() {
|
||||
val paths = listOf(PathTag("/index.html", h1), PathTag("/app.js", h2))
|
||||
val aggregate = SiteAggregateHash.compute(paths)
|
||||
|
||||
// No x tag declared -> nothing to enforce.
|
||||
assertTrue(SiteAggregateHash.verify(paths, null))
|
||||
// Declared matches, case-insensitively.
|
||||
assertTrue(SiteAggregateHash.verify(paths, aggregate))
|
||||
assertTrue(SiteAggregateHash.verify(paths, aggregate.uppercase()))
|
||||
// Declared aggregate that doesn't match the paths is rejected.
|
||||
assertFalse(SiteAggregateHash.verify(paths, h1))
|
||||
// Tampering a single path hash changes the recomputed aggregate -> mismatch.
|
||||
val tampered = listOf(PathTag("/index.html", h2), PathTag("/app.js", h2))
|
||||
assertFalse(SiteAggregateHash.verify(tampered, aggregate))
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip5dNapplets
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NappletEventTest {
|
||||
private val zero = "00".repeat(32)
|
||||
private val h1 = "11".repeat(32)
|
||||
private val h2 = "22".repeat(32)
|
||||
private val paths = listOf(PathTag("/index.html", h1), PathTag("/app.js", h2))
|
||||
private val servers = listOf("https://cdn.example.com")
|
||||
private val requires = listOf("relay", "identity")
|
||||
|
||||
private fun <T : Event> materialize(
|
||||
template: EventTemplate<T>,
|
||||
factory: (String, String, Long, Array<Array<String>>, String, String) -> T,
|
||||
): T = factory(zero, zero, template.createdAt, template.tags, template.content, zero)
|
||||
|
||||
@Test
|
||||
fun namedNappletRoundTrips() {
|
||||
val event =
|
||||
materialize(
|
||||
NamedNappletEvent.build(
|
||||
identifier = "calculator",
|
||||
paths = paths,
|
||||
servers = servers,
|
||||
requires = requires,
|
||||
title = "Calc",
|
||||
description = "a calculator",
|
||||
source = "https://github.com/x/calc",
|
||||
),
|
||||
::NamedNappletEvent,
|
||||
)
|
||||
|
||||
assertEquals(35129, event.kind)
|
||||
assertEquals("calculator", event.identifier())
|
||||
assertEquals(paths.map { it.path }, event.paths().map { it.path })
|
||||
assertEquals(paths.map { it.hash }, event.paths().map { it.hash })
|
||||
assertEquals(servers, event.servers())
|
||||
assertEquals(requires, event.requires())
|
||||
assertEquals("Calc", event.title())
|
||||
assertEquals("a calculator", event.description())
|
||||
assertEquals("https://github.com/x/calc", event.source())
|
||||
|
||||
// build() stamps the x aggregate, and it verifies against the path tags.
|
||||
assertNotNull(event.declaredAggregateHash())
|
||||
assertEquals(event.computeAggregateHash(), event.declaredAggregateHash())
|
||||
assertTrue(event.verifyAggregate())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rootNappletHasKind15129AndNoIdentifierNeeded() {
|
||||
val event = materialize(RootNappletEvent.build(paths = paths), ::RootNappletEvent)
|
||||
assertEquals(15129, event.kind)
|
||||
assertTrue(event.verifyAggregate())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snapshotHasKind5129AndAlwaysCarriesAggregate() {
|
||||
val event = materialize(NappletSnapshotEvent.build(paths = paths), ::NappletSnapshotEvent)
|
||||
assertEquals(5129, event.kind)
|
||||
assertNotNull(event.declaredAggregateHash())
|
||||
assertTrue(event.verifyAggregate())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tamperedPathBreaksAggregateVerification() {
|
||||
// Build a valid manifest, then rewrite one path hash in the tags without
|
||||
// touching the x tag — the recomputed aggregate no longer matches.
|
||||
val template = NamedNappletEvent.build(identifier = "x", paths = paths)
|
||||
val tamperedTags =
|
||||
template.tags
|
||||
.map { tag ->
|
||||
if (tag.getOrNull(0) == PathTag.TAG_NAME && tag.getOrNull(1) == "/app.js") {
|
||||
arrayOf(PathTag.TAG_NAME, "/app.js", h1)
|
||||
} else {
|
||||
tag
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
val tampered = NamedNappletEvent(zero, zero, template.createdAt, tamperedTags, template.content, zero)
|
||||
assertFalse(tampered.verifyAggregate())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun eventFactoryRoutesAllThreeNappletKinds() {
|
||||
val named = NamedNappletEvent.build(identifier = "x", paths = paths)
|
||||
val root = RootNappletEvent.build(paths = paths)
|
||||
val snapshot = NappletSnapshotEvent.build(paths = paths)
|
||||
|
||||
assertIs<NamedNappletEvent>(
|
||||
EventFactory.create<Event>(zero, zero, named.createdAt, NamedNappletEvent.KIND, named.tags, named.content, zero),
|
||||
)
|
||||
assertIs<RootNappletEvent>(
|
||||
EventFactory.create<Event>(zero, zero, root.createdAt, RootNappletEvent.KIND, root.tags, root.content, zero),
|
||||
)
|
||||
assertIs<NappletSnapshotEvent>(
|
||||
EventFactory.create<Event>(zero, zero, snapshot.createdAt, NappletSnapshotEvent.KIND, snapshot.tags, snapshot.content, zero),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyOptionalsAreOmitted() {
|
||||
val event = materialize(RootNappletEvent.build(paths = paths), ::RootNappletEvent)
|
||||
assertTrue(event.servers().isEmpty())
|
||||
assertTrue(event.requires().isEmpty())
|
||||
assertEquals(null, event.title())
|
||||
// Only path + x (+ alt) tags expected; the manifest still has its paths.
|
||||
assertContentEquals(paths.map { it.hash }, event.paths().map { it.hash })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user