nostr-tools 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +5 -1
  2. package/lib/cjs/abstract-pool.js +9 -6
  3. package/lib/cjs/abstract-pool.js.map +2 -2
  4. package/lib/cjs/abstract-relay.js +9 -6
  5. package/lib/cjs/abstract-relay.js.map +2 -2
  6. package/lib/cjs/filter.js.map +2 -2
  7. package/lib/cjs/index.js +60 -53
  8. package/lib/cjs/index.js.map +3 -3
  9. package/lib/cjs/kinds.js.map +2 -2
  10. package/lib/cjs/nip04.js.map +2 -2
  11. package/lib/cjs/nip11.js.map +2 -2
  12. package/lib/cjs/nip13.js.map +2 -2
  13. package/lib/cjs/nip18.js.map +2 -2
  14. package/lib/cjs/nip19.js.map +2 -2
  15. package/lib/cjs/nip21.js.map +2 -2
  16. package/lib/cjs/nip25.js.map +2 -2
  17. package/lib/cjs/nip27.js.map +2 -2
  18. package/lib/cjs/nip28.js.map +2 -2
  19. package/lib/cjs/nip29.js.map +2 -2
  20. package/lib/cjs/nip30.js.map +2 -2
  21. package/lib/cjs/nip42.js.map +1 -1
  22. package/lib/cjs/nip44.js +51 -47
  23. package/lib/cjs/nip44.js.map +2 -2
  24. package/lib/cjs/nip46.js +20 -9
  25. package/lib/cjs/nip46.js.map +3 -3
  26. package/lib/cjs/nip47.js.map +2 -2
  27. package/lib/cjs/nip57.js.map +2 -2
  28. package/lib/cjs/nip75.js.map +1 -1
  29. package/lib/cjs/nip94.js.map +1 -1
  30. package/lib/cjs/nip96.js +3 -5
  31. package/lib/cjs/nip96.js.map +2 -2
  32. package/lib/cjs/nip98.js.map +2 -2
  33. package/lib/cjs/nip99.js.map +1 -1
  34. package/lib/cjs/pool.js +9 -6
  35. package/lib/cjs/pool.js.map +2 -2
  36. package/lib/cjs/pure.js.map +2 -2
  37. package/lib/cjs/references.js.map +2 -2
  38. package/lib/cjs/relay.js +9 -6
  39. package/lib/cjs/relay.js.map +2 -2
  40. package/lib/cjs/utils.js.map +2 -2
  41. package/lib/esm/abstract-pool.js +9 -6
  42. package/lib/esm/abstract-pool.js.map +2 -2
  43. package/lib/esm/abstract-relay.js +9 -6
  44. package/lib/esm/abstract-relay.js.map +2 -2
  45. package/lib/esm/filter.js.map +2 -2
  46. package/lib/esm/index.js +60 -53
  47. package/lib/esm/index.js.map +3 -3
  48. package/lib/esm/kinds.js.map +2 -2
  49. package/lib/esm/nip04.js.map +2 -2
  50. package/lib/esm/nip11.js.map +2 -2
  51. package/lib/esm/nip13.js.map +2 -2
  52. package/lib/esm/nip18.js.map +2 -2
  53. package/lib/esm/nip19.js.map +2 -2
  54. package/lib/esm/nip21.js.map +2 -2
  55. package/lib/esm/nip25.js.map +2 -2
  56. package/lib/esm/nip27.js.map +2 -2
  57. package/lib/esm/nip28.js.map +2 -2
  58. package/lib/esm/nip29.js.map +2 -2
  59. package/lib/esm/nip30.js.map +2 -2
  60. package/lib/esm/nip42.js.map +1 -1
  61. package/lib/esm/nip44.js +53 -47
  62. package/lib/esm/nip44.js.map +2 -2
  63. package/lib/esm/nip46.js +20 -9
  64. package/lib/esm/nip46.js.map +3 -3
  65. package/lib/esm/nip47.js.map +2 -2
  66. package/lib/esm/nip57.js.map +2 -2
  67. package/lib/esm/nip75.js.map +1 -1
  68. package/lib/esm/nip94.js.map +1 -1
  69. package/lib/esm/nip96.js +5 -5
  70. package/lib/esm/nip96.js.map +3 -3
  71. package/lib/esm/nip98.js.map +2 -2
  72. package/lib/esm/nip99.js.map +1 -1
  73. package/lib/esm/pool.js +9 -6
  74. package/lib/esm/pool.js.map +2 -2
  75. package/lib/esm/pure.js.map +2 -2
  76. package/lib/esm/references.js.map +2 -2
  77. package/lib/esm/relay.js +9 -6
  78. package/lib/esm/relay.js.map +2 -2
  79. package/lib/esm/utils.js.map +2 -2
  80. package/lib/nostr.bundle.js +60 -53
  81. package/lib/nostr.bundle.js.map +3 -3
  82. package/lib/types/abstract-relay.d.ts +1 -0
  83. package/lib/types/nip44.d.ts +27 -53
  84. package/lib/types/nip46.d.ts +2 -1
  85. package/lib/types/nip47.d.ts +6 -3
  86. package/lib/types/nip96.d.ts +2 -0
  87. package/lib/types/test-helpers.d.ts +0 -1
  88. package/lib/types/utils.d.ts +1 -0
  89. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../nip75.ts", "../../kinds.ts"],
4
- "sourcesContent": ["import { Event, EventTemplate } from './core'\nimport { ZapGoal } from './kinds'\n\n/**\n * Represents a fundraising goal in the Nostr network as defined by NIP-75.\n * This type is used to structure the information needed to create a goal event (`kind:9041`).\n */\nexport type Goal = {\n /**\n * A human-readable description of the fundraising goal.\n * This content should provide clear information about the purpose of the fundraising.\n */\n content: string\n\n /**\n * The target amount for the fundraising goal in milisats.\n * This defines the financial target that the fundraiser aims to reach.\n */\n amount: string\n\n /**\n * A list of relays where the zaps towards this goal will be sent to and tallied from.\n * Each relay is represented by its WebSocket URL.\n */\n relays: string[]\n\n /**\n * An optional timestamp (in seconds, UNIX epoch) indicating when the fundraising goal is considered closed.\n * Zaps published after this timestamp should not count towards the goal progress.\n * If not provided, the goal remains open indefinitely or until manually closed.\n */\n closedAt?: number\n\n /**\n * An optional URL to an image related to the goal.\n * This can be used to visually represent the goal on client interfaces.\n */\n image?: string\n\n /**\n * An optional brief description or summary of the goal.\n * This can provide a quick overview of the goal, separate from the detailed `content`.\n */\n summary?: string\n\n /**\n * An optional URL related to the goal, providing additional information or actions through an 'r' tag.\n * This is a single URL, as per NIP-75 specifications for linking additional resources.\n */\n r?: string\n\n /**\n * An optional parameterized replaceable event linked to the goal, specified through an 'a' tag.\n * This is a single event id, aligning with NIP-75's allowance for linking to specific events.\n */\n a?: string\n\n /**\n * Optional tags specifying multiple beneficiary pubkeys or additional criteria for zapping,\n * allowing contributions to be directed towards multiple recipients or according to specific conditions.\n */\n zapTags?: string[][]\n}\n\n/**\n * Generates an EventTemplate for a fundraising goal based on the provided ZapGoal object.\n * This function is tailored to fit the structure of EventTemplate as defined in the library.\n * @param zapGoal The ZapGoal object containing the details of the fundraising goal.\n * @returns An EventTemplate object structured for creating a Nostr event.\n */\nexport function generateGoalEventTemplate({\n amount,\n content,\n relays,\n a,\n closedAt,\n image,\n r,\n summary,\n zapTags,\n}: Goal): EventTemplate {\n const tags: string[][] = [\n ['amount', amount],\n ['relays', ...relays],\n ]\n\n // Append optional tags based on the presence of optional properties in zapGoal\n closedAt && tags.push(['closed_at', closedAt.toString()])\n image && tags.push(['image', image])\n summary && tags.push(['summary', summary])\n r && tags.push(['r', r])\n a && tags.push(['a', a])\n zapTags && tags.push(...zapTags)\n\n // Construct the EventTemplate object\n const eventTemplate: EventTemplate = {\n created_at: Math.floor(Date.now() / 1000),\n kind: ZapGoal,\n content,\n tags,\n }\n\n return eventTemplate\n}\n\nexport function validateZapGoalEvent(event: Event): boolean {\n if (event.kind !== ZapGoal) return false\n\n const requiredTags = ['amount', 'relays'] as const\n for (const tag of requiredTags) {\n if (!event.tags.find(([t]) => t == tag)) return false\n }\n\n return true\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number) {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number) {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number) {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number) {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
4
+ "sourcesContent": ["import { Event, EventTemplate } from './core'\nimport { ZapGoal } from './kinds'\n\n/**\n * Represents a fundraising goal in the Nostr network as defined by NIP-75.\n * This type is used to structure the information needed to create a goal event (`kind:9041`).\n */\nexport type Goal = {\n /**\n * A human-readable description of the fundraising goal.\n * This content should provide clear information about the purpose of the fundraising.\n */\n content: string\n\n /**\n * The target amount for the fundraising goal in milisats.\n * This defines the financial target that the fundraiser aims to reach.\n */\n amount: string\n\n /**\n * A list of relays where the zaps towards this goal will be sent to and tallied from.\n * Each relay is represented by its WebSocket URL.\n */\n relays: string[]\n\n /**\n * An optional timestamp (in seconds, UNIX epoch) indicating when the fundraising goal is considered closed.\n * Zaps published after this timestamp should not count towards the goal progress.\n * If not provided, the goal remains open indefinitely or until manually closed.\n */\n closedAt?: number\n\n /**\n * An optional URL to an image related to the goal.\n * This can be used to visually represent the goal on client interfaces.\n */\n image?: string\n\n /**\n * An optional brief description or summary of the goal.\n * This can provide a quick overview of the goal, separate from the detailed `content`.\n */\n summary?: string\n\n /**\n * An optional URL related to the goal, providing additional information or actions through an 'r' tag.\n * This is a single URL, as per NIP-75 specifications for linking additional resources.\n */\n r?: string\n\n /**\n * An optional parameterized replaceable event linked to the goal, specified through an 'a' tag.\n * This is a single event id, aligning with NIP-75's allowance for linking to specific events.\n */\n a?: string\n\n /**\n * Optional tags specifying multiple beneficiary pubkeys or additional criteria for zapping,\n * allowing contributions to be directed towards multiple recipients or according to specific conditions.\n */\n zapTags?: string[][]\n}\n\n/**\n * Generates an EventTemplate for a fundraising goal based on the provided ZapGoal object.\n * This function is tailored to fit the structure of EventTemplate as defined in the library.\n * @param zapGoal The ZapGoal object containing the details of the fundraising goal.\n * @returns An EventTemplate object structured for creating a Nostr event.\n */\nexport function generateGoalEventTemplate({\n amount,\n content,\n relays,\n a,\n closedAt,\n image,\n r,\n summary,\n zapTags,\n}: Goal): EventTemplate {\n const tags: string[][] = [\n ['amount', amount],\n ['relays', ...relays],\n ]\n\n // Append optional tags based on the presence of optional properties in zapGoal\n closedAt && tags.push(['closed_at', closedAt.toString()])\n image && tags.push(['image', image])\n summary && tags.push(['summary', summary])\n r && tags.push(['r', r])\n a && tags.push(['a', a])\n zapTags && tags.push(...zapTags)\n\n // Construct the EventTemplate object\n const eventTemplate: EventTemplate = {\n created_at: Math.floor(Date.now() / 1000),\n kind: ZapGoal,\n content,\n tags,\n }\n\n return eventTemplate\n}\n\nexport function validateZapGoalEvent(event: Event): boolean {\n if (event.kind !== ZapGoal) return false\n\n const requiredTags = ['amount', 'relays'] as const\n for (const tag of requiredTags) {\n if (!event.tags.find(([t]) => t == tag)) return false\n }\n\n return true\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number): boolean {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number): boolean {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number): boolean {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number): boolean {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2DO,IAAM,UAAU;;;ADWhB,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,OAAmB;AAAA,IACvB,CAAC,UAAU,MAAM;AAAA,IACjB,CAAC,UAAU,GAAG,MAAM;AAAA,EACtB;AAGA,cAAY,KAAK,KAAK,CAAC,aAAa,SAAS,SAAS,CAAC,CAAC;AACxD,WAAS,KAAK,KAAK,CAAC,SAAS,KAAK,CAAC;AACnC,aAAW,KAAK,KAAK,CAAC,WAAW,OAAO,CAAC;AACzC,OAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC;AACvB,OAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC;AACvB,aAAW,KAAK,KAAK,GAAG,OAAO;AAG/B,QAAM,gBAA+B;AAAA,IACnC,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,IACxC,MAAM;AAAA,IACN;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,qBAAqB,OAAuB;AAC1D,MAAI,MAAM,SAAS;AAAS,WAAO;AAEnC,QAAM,eAAe,CAAC,UAAU,QAAQ;AACxC,aAAW,OAAO,cAAc;AAC9B,QAAI,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,GAAG;AAAG,aAAO;AAAA,EAClD;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../nip94.ts", "../../kinds.ts"],
4
- "sourcesContent": ["import { Event, EventTemplate } from './core'\nimport { FileMetadata as FileMetadataKind } from './kinds.ts'\n\n/**\n * Type definition for File Metadata as specified in NIP-94.\n * This type is used to represent the metadata associated with a file sharing event (kind: 1063).\n */\nexport type FileMetadataObject = {\n /**\n * A description or caption for the file content.\n */\n content: string\n\n /**\n * The URL to download the file.\n */\n url: string\n\n /**\n * The MIME type of the file, in lowercase.\n */\n m: string\n\n /**\n * The SHA-256 hex-encoded string of the file.\n */\n x: string\n\n /**\n * The SHA-256 hex-encoded string of the original file, before any transformations done by the upload server.\n */\n ox: string\n\n /**\n * Optional: The size of the file in bytes.\n */\n size?: string\n\n /**\n * Optional: The dimensions of the file in pixels, in the format \"<width>x<height>\".\n */\n dim?: string\n\n /**\n * Optional: The URI to the magnet file.\n */\n magnet?: string\n\n /**\n * Optional: The torrent infohash.\n */\n i?: string\n\n /**\n * Optional: The blurhash string to show while the file is being loaded by the client.\n */\n blurhash?: string\n\n /**\n * Optional: The URL of the thumbnail image with the same aspect ratio as the original file.\n */\n thumb?: string\n\n /**\n * Optional: The URL of a preview image with the same dimensions as the original file.\n */\n image?: string\n\n /**\n * Optional: A text excerpt or summary of the file's content.\n */\n summary?: string\n\n /**\n * Optional: A description for accessibility, providing context or a brief description of the file.\n */\n alt?: string\n}\n\n/**\n * Generates an event template based on a file metadata object.\n *\n * @param fileMetadata - The file metadata object.\n * @returns The event template.\n */\nexport function generateEventTemplate(fileMetadata: FileMetadataObject): EventTemplate {\n const eventTemplate: EventTemplate = {\n content: fileMetadata.content,\n created_at: Math.floor(Date.now() / 1000),\n kind: FileMetadataKind,\n tags: [\n ['url', fileMetadata.url],\n ['m', fileMetadata.m],\n ['x', fileMetadata.x],\n ['ox', fileMetadata.ox],\n ],\n }\n\n if (fileMetadata.size) eventTemplate.tags.push(['size', fileMetadata.size])\n if (fileMetadata.dim) eventTemplate.tags.push(['dim', fileMetadata.dim])\n if (fileMetadata.i) eventTemplate.tags.push(['i', fileMetadata.i])\n if (fileMetadata.blurhash) eventTemplate.tags.push(['blurhash', fileMetadata.blurhash])\n if (fileMetadata.thumb) eventTemplate.tags.push(['thumb', fileMetadata.thumb])\n if (fileMetadata.image) eventTemplate.tags.push(['image', fileMetadata.image])\n if (fileMetadata.summary) eventTemplate.tags.push(['summary', fileMetadata.summary])\n if (fileMetadata.alt) eventTemplate.tags.push(['alt', fileMetadata.alt])\n\n return eventTemplate\n}\n\n/**\n * Validates an event to ensure it is a valid file metadata event.\n * @param event - The event to validate.\n * @returns True if the event is valid, false otherwise.\n */\nexport function validateEvent(event: Event): boolean {\n if (event.kind !== FileMetadataKind) return false\n\n if (!event.content) return false\n\n const requiredTags = ['url', 'm', 'x', 'ox'] as const\n for (const tag of requiredTags) {\n if (!event.tags.find(([t]) => t == tag)) return false\n }\n\n // validate optional size tag\n const sizeTag = event.tags.find(([t]) => t == 'size')\n if (sizeTag && isNaN(Number(sizeTag[1]))) return false\n\n // validate optional dim tag\n const dimTag = event.tags.find(([t]) => t == 'dim')\n if (dimTag && !dimTag[1].match(/^\\d+x\\d+$/)) return false\n\n return true\n}\n\n/**\n * Parses an event and returns a file metadata object.\n * @param event - The event to parse.\n * @returns The file metadata object.\n * @throws Error if the event is invalid.\n */\nexport function parseEvent(event: Event): FileMetadataObject {\n if (!validateEvent(event)) {\n throw new Error('Invalid event')\n }\n\n const fileMetadata: FileMetadataObject = {\n content: event.content,\n url: '',\n m: '',\n x: '',\n ox: '',\n }\n\n for (const [tag, value] of event.tags) {\n switch (tag) {\n case 'url':\n fileMetadata.url = value\n break\n case 'm':\n fileMetadata.m = value\n break\n case 'x':\n fileMetadata.x = value\n break\n case 'ox':\n fileMetadata.ox = value\n break\n case 'size':\n fileMetadata.size = value\n break\n case 'dim':\n fileMetadata.dim = value\n break\n case 'magnet':\n fileMetadata.magnet = value\n break\n case 'i':\n fileMetadata.i = value\n break\n case 'blurhash':\n fileMetadata.blurhash = value\n break\n case 'thumb':\n fileMetadata.thumb = value\n break\n case 'image':\n fileMetadata.image = value\n break\n case 'summary':\n fileMetadata.summary = value\n break\n case 'alt':\n fileMetadata.alt = value\n break\n }\n }\n\n return fileMetadata\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number) {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number) {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number) {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number) {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
4
+ "sourcesContent": ["import { Event, EventTemplate } from './core'\nimport { FileMetadata as FileMetadataKind } from './kinds.ts'\n\n/**\n * Type definition for File Metadata as specified in NIP-94.\n * This type is used to represent the metadata associated with a file sharing event (kind: 1063).\n */\nexport type FileMetadataObject = {\n /**\n * A description or caption for the file content.\n */\n content: string\n\n /**\n * The URL to download the file.\n */\n url: string\n\n /**\n * The MIME type of the file, in lowercase.\n */\n m: string\n\n /**\n * The SHA-256 hex-encoded string of the file.\n */\n x: string\n\n /**\n * The SHA-256 hex-encoded string of the original file, before any transformations done by the upload server.\n */\n ox: string\n\n /**\n * Optional: The size of the file in bytes.\n */\n size?: string\n\n /**\n * Optional: The dimensions of the file in pixels, in the format \"<width>x<height>\".\n */\n dim?: string\n\n /**\n * Optional: The URI to the magnet file.\n */\n magnet?: string\n\n /**\n * Optional: The torrent infohash.\n */\n i?: string\n\n /**\n * Optional: The blurhash string to show while the file is being loaded by the client.\n */\n blurhash?: string\n\n /**\n * Optional: The URL of the thumbnail image with the same aspect ratio as the original file.\n */\n thumb?: string\n\n /**\n * Optional: The URL of a preview image with the same dimensions as the original file.\n */\n image?: string\n\n /**\n * Optional: A text excerpt or summary of the file's content.\n */\n summary?: string\n\n /**\n * Optional: A description for accessibility, providing context or a brief description of the file.\n */\n alt?: string\n}\n\n/**\n * Generates an event template based on a file metadata object.\n *\n * @param fileMetadata - The file metadata object.\n * @returns The event template.\n */\nexport function generateEventTemplate(fileMetadata: FileMetadataObject): EventTemplate {\n const eventTemplate: EventTemplate = {\n content: fileMetadata.content,\n created_at: Math.floor(Date.now() / 1000),\n kind: FileMetadataKind,\n tags: [\n ['url', fileMetadata.url],\n ['m', fileMetadata.m],\n ['x', fileMetadata.x],\n ['ox', fileMetadata.ox],\n ],\n }\n\n if (fileMetadata.size) eventTemplate.tags.push(['size', fileMetadata.size])\n if (fileMetadata.dim) eventTemplate.tags.push(['dim', fileMetadata.dim])\n if (fileMetadata.i) eventTemplate.tags.push(['i', fileMetadata.i])\n if (fileMetadata.blurhash) eventTemplate.tags.push(['blurhash', fileMetadata.blurhash])\n if (fileMetadata.thumb) eventTemplate.tags.push(['thumb', fileMetadata.thumb])\n if (fileMetadata.image) eventTemplate.tags.push(['image', fileMetadata.image])\n if (fileMetadata.summary) eventTemplate.tags.push(['summary', fileMetadata.summary])\n if (fileMetadata.alt) eventTemplate.tags.push(['alt', fileMetadata.alt])\n\n return eventTemplate\n}\n\n/**\n * Validates an event to ensure it is a valid file metadata event.\n * @param event - The event to validate.\n * @returns True if the event is valid, false otherwise.\n */\nexport function validateEvent(event: Event): boolean {\n if (event.kind !== FileMetadataKind) return false\n\n if (!event.content) return false\n\n const requiredTags = ['url', 'm', 'x', 'ox'] as const\n for (const tag of requiredTags) {\n if (!event.tags.find(([t]) => t == tag)) return false\n }\n\n // validate optional size tag\n const sizeTag = event.tags.find(([t]) => t == 'size')\n if (sizeTag && isNaN(Number(sizeTag[1]))) return false\n\n // validate optional dim tag\n const dimTag = event.tags.find(([t]) => t == 'dim')\n if (dimTag && !dimTag[1].match(/^\\d+x\\d+$/)) return false\n\n return true\n}\n\n/**\n * Parses an event and returns a file metadata object.\n * @param event - The event to parse.\n * @returns The file metadata object.\n * @throws Error if the event is invalid.\n */\nexport function parseEvent(event: Event): FileMetadataObject {\n if (!validateEvent(event)) {\n throw new Error('Invalid event')\n }\n\n const fileMetadata: FileMetadataObject = {\n content: event.content,\n url: '',\n m: '',\n x: '',\n ox: '',\n }\n\n for (const [tag, value] of event.tags) {\n switch (tag) {\n case 'url':\n fileMetadata.url = value\n break\n case 'm':\n fileMetadata.m = value\n break\n case 'x':\n fileMetadata.x = value\n break\n case 'ox':\n fileMetadata.ox = value\n break\n case 'size':\n fileMetadata.size = value\n break\n case 'dim':\n fileMetadata.dim = value\n break\n case 'magnet':\n fileMetadata.magnet = value\n break\n case 'i':\n fileMetadata.i = value\n break\n case 'blurhash':\n fileMetadata.blurhash = value\n break\n case 'thumb':\n fileMetadata.thumb = value\n break\n case 'image':\n fileMetadata.image = value\n break\n case 'summary':\n fileMetadata.summary = value\n break\n case 'alt':\n fileMetadata.alt = value\n break\n }\n }\n\n return fileMetadata\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number): boolean {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number): boolean {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number): boolean {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number): boolean {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
package/lib/cjs/nip96.js CHANGED
@@ -32,11 +32,13 @@ __export(nip96_exports, {
32
32
  validateServerConfiguration: () => validateServerConfiguration
33
33
  });
34
34
  module.exports = __toCommonJS(nip96_exports);
35
+ var import_sha256 = require("@noble/hashes/sha256");
35
36
 
36
37
  // kinds.ts
37
38
  var FileServerPreference = 10096;
38
39
 
39
40
  // nip96.ts
41
+ var import_utils = require("@noble/hashes/utils");
40
42
  function validateServerConfiguration(config) {
41
43
  if (Boolean(config.api_url) == false) {
42
44
  return false;
@@ -244,9 +246,5 @@ function generateFSPEventTemplate(serverUrls) {
244
246
  };
245
247
  }
246
248
  async function calculateFileHash(file) {
247
- const buffer = await file.arrayBuffer();
248
- const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
249
- const hashArray = Array.from(new Uint8Array(hashBuffer));
250
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
251
- return hashHex;
249
+ return (0, import_utils.bytesToHex)((0, import_sha256.sha256)(new Uint8Array(await file.arrayBuffer())));
252
250
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../nip96.ts", "../../kinds.ts"],
4
- "sourcesContent": ["import { EventTemplate } from './core'\nimport { FileServerPreference } from './kinds'\n\n/**\n * Represents the configuration for a server compliant with NIP-96.\n */\nexport type ServerConfiguration = {\n /**\n * The base URL from which file upload and deletion operations are served.\n * Also used for downloads if \"download_url\" is not specified.\n */\n api_url: string\n\n /**\n * Optional. The base URL from which files are downloaded.\n * Used if different from the \"api_url\".\n */\n download_url?: string\n\n /**\n * Optional. URL of another HTTP file storage server's configuration.\n * Used by nostr relays to delegate to another server.\n * In this case, \"api_url\" must be an empty string.\n */\n delegated_to_url?: string\n\n /**\n * Optional. An array of NIP numbers that this server supports.\n */\n supported_nips?: number[]\n\n /**\n * Optional. URL to the server's Terms of Service.\n */\n tos_url?: string\n\n /**\n * Optional. An array of MIME types supported by the server.\n */\n content_types?: string[]\n\n /**\n * Optional. Defines various storage plans offered by the server.\n */\n plans?: {\n [planKey: string]: {\n /**\n * The name of the storage plan.\n */\n name: string\n\n /**\n * Optional. Indicates whether NIP-98 is required for uploads in this plan.\n */\n is_nip98_required?: boolean\n\n /**\n * Optional. URL to a landing page providing more information about the plan.\n */\n url?: string\n\n /**\n * Optional. The maximum file size allowed under this plan, in bytes.\n */\n max_byte_size?: number\n\n /**\n * Optional. Defines the range of file expiration in days.\n * The first value indicates the minimum expiration time, and the second value indicates the maximum.\n * A value of 0 indicates no expiration.\n */\n file_expiration?: [number, number]\n\n /**\n * Optional. Specifies the types of media transformations supported under this plan.\n * Currently, only image transformations are considered.\n */\n media_transformations?: {\n /**\n * Optional. An array of supported image transformation types.\n */\n image?: string[]\n }\n }\n }\n}\n\n/**\n * Represents the optional form data fields for file upload in accordance with NIP-96.\n */\nexport type OptionalFormDataFields = {\n /**\n * Specifies the desired expiration time of the file on the server.\n * It should be a string representing a UNIX timestamp in seconds.\n * An empty string indicates that the file should be stored indefinitely.\n */\n expiration?: string\n\n /**\n * Indicates the size of the file in bytes.\n * This field can be used by the server to pre-validate the file size before processing the upload.\n */\n size?: string\n\n /**\n * Provides a strict description of the file for accessibility purposes,\n * particularly useful for visibility-impaired users.\n */\n alt?: string\n\n /**\n * A loose, more descriptive caption for the file.\n * This can be used for additional context or commentary about the file.\n */\n caption?: string\n\n /**\n * Specifies the intended use of the file.\n * Can be either 'avatar' or 'banner', indicating if the file is to be used as an avatar or a banner.\n * Absence of this field suggests standard file upload without special treatment.\n */\n media_type?: 'avatar' | 'banner'\n\n /**\n * The MIME type of the file being uploaded.\n * This can be used for early rejection by the server if the file type isn't supported.\n */\n content_type?: string\n\n /**\n * Other custom form data fields.\n */\n [key: string]: string | undefined\n}\n\n/**\n * Type representing the response from a NIP-96 compliant server after a file upload request.\n */\nexport type FileUploadResponse = {\n /**\n * The status of the upload request.\n * - 'success': Indicates the file was successfully uploaded.\n * - 'error': Indicates there was an error in the upload process.\n * - 'processing': Indicates the file is still being processed (used in cases of delayed processing).\n */\n status: 'success' | 'error' | 'processing'\n\n /**\n * A message provided by the server, which could be a success message, error description, or processing status.\n */\n message: string\n\n /**\n * Optional. A URL provided by the server where the upload processing status can be checked.\n * This is relevant in cases where the file upload involves delayed processing.\n */\n processing_url?: string\n\n /**\n * Optional. An event object conforming to NIP-94, which includes details about the uploaded file.\n * This object is typically provided in the response for a successful upload and contains\n * essential information such as the download URL and file metadata.\n */\n nip94_event?: {\n /**\n * A collection of key-value pairs (tags) providing metadata about the uploaded file.\n * Standard tags include:\n * - 'url': The URL where the file can be accessed.\n * - 'ox': The SHA-256 hash of the original file before any server-side transformations.\n * Additional optional tags might include file dimensions, MIME type, etc.\n */\n tags: Array<[string, string]>\n\n /**\n * A content field, which is typically empty for file upload events but included for consistency with the NIP-94 structure.\n */\n content: string\n }\n}\n\n/**\n * Type representing the response from a NIP-96 compliant server after a delayed processing request.\n */\nexport type DelayedProcessingResponse = {\n /**\n * The status of the delayed processing request.\n * - 'processing': Indicates the file is still being processed.\n * - 'error': Indicates there was an error in the processing.\n */\n status: 'processing' | 'error'\n\n /**\n * A message provided by the server, which could be a success message or error description.\n */\n message: string\n\n /**\n * The percentage of the file that has been processed. This is a number between 0 and 100.\n */\n percentage: number\n}\n\n/**\n * Validates the server configuration.\n *\n * @param config - The server configuration object.\n * @returns True if the configuration is valid, false otherwise.\n */\nexport function validateServerConfiguration(config: ServerConfiguration): boolean {\n if (Boolean(config.api_url) == false) {\n return false\n }\n\n if (Boolean(config.delegated_to_url) && Boolean(config.api_url)) {\n return false\n }\n\n return true\n}\n\n/**\n * Fetches, parses, and validates the server configuration from the given URL.\n *\n * @param serverUrl The URL of the server.\n * @returns The server configuration, or an error if the configuration could not be fetched or parsed.\n */\nexport async function readServerConfig(serverUrl: string): Promise<ServerConfiguration> {\n const HTTPROUTE = '/.well-known/nostr/nip96.json' as const\n let fetchUrl = ''\n\n try {\n const { origin } = new URL(serverUrl)\n fetchUrl = origin + HTTPROUTE\n } catch (error) {\n throw new Error('Invalid URL')\n }\n\n try {\n const response = await fetch(fetchUrl)\n\n if (!response.ok) {\n throw new Error(`Error fetching ${fetchUrl}: ${response.statusText}`)\n }\n\n const data: any = await response.json()\n\n if (!data) {\n throw new Error('No data')\n }\n\n if (!validateServerConfiguration(data)) {\n throw new Error('Invalid configuration data')\n }\n\n return data\n } catch (_) {\n throw new Error(`Error fetching.`)\n }\n}\n\n/**\n * Validates if the given object is a valid FileUploadResponse.\n *\n * @param response - The object to validate.\n * @returns true if the object is a valid FileUploadResponse, otherwise false.\n */\nexport function validateFileUploadResponse(response: any): response is FileUploadResponse {\n if (typeof response !== 'object' || response === null) return false\n\n if (!response.status || !response.message) {\n return false\n }\n\n if (response.status !== 'success' && response.status !== 'error' && response.status !== 'processing') {\n return false\n }\n\n if (typeof response.message !== 'string') {\n return false\n }\n\n if (response.status === 'processing' && !response.processing_url) {\n return false\n }\n\n if (response.processing_url) {\n if (typeof response.processing_url !== 'string') {\n return false\n }\n }\n\n if (response.status === 'success' && !response.nip94_event) {\n return false\n }\n\n if (response.nip94_event) {\n if (\n !response.nip94_event.tags ||\n !Array.isArray(response.nip94_event.tags) ||\n response.nip94_event.tags.length === 0\n ) {\n return false\n }\n\n for (const tag of response.nip94_event.tags) {\n if (!Array.isArray(tag) || tag.length !== 2) return false\n\n if (typeof tag[0] !== 'string' || typeof tag[1] !== 'string') return false\n }\n\n if (!(response.nip94_event.tags as string[]).find(t => t[0] === 'url')) {\n return false\n }\n\n if (!(response.nip94_event.tags as string[]).find(t => t[0] === 'ox')) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Uploads a file to a NIP-96 compliant server.\n *\n * @param file - The file to be uploaded.\n * @param serverApiUrl - The API URL of the server, retrieved from the server's configuration.\n * @param nip98AuthorizationHeader - The authorization header from NIP-98.\n * @param optionalFormDataFields - Optional form data fields.\n * @returns A promise that resolves to the server's response.\n */\nexport async function uploadFile(\n file: File,\n serverApiUrl: string,\n nip98AuthorizationHeader: string,\n optionalFormDataFields?: OptionalFormDataFields,\n): Promise<FileUploadResponse> {\n // Create FormData object\n const formData = new FormData()\n\n // Append the authorization header to HTML Form Data\n formData.append('Authorization', nip98AuthorizationHeader)\n\n // Append optional fields to FormData\n optionalFormDataFields &&\n Object.entries(optionalFormDataFields).forEach(([key, value]) => {\n if (value) {\n formData.append(key, value)\n }\n })\n\n // Append the file to FormData as the last field\n formData.append('file', file)\n\n // Make the POST request to the server\n const response = await fetch(serverApiUrl, {\n method: 'POST',\n headers: {\n Authorization: nip98AuthorizationHeader,\n 'Content-Type': 'multipart/form-data',\n },\n body: formData,\n })\n\n if (response.ok === false) {\n // 413 Payload Too Large\n if (response.status === 413) {\n throw new Error('File too large!')\n }\n\n // 400 Bad Request\n if (response.status === 400) {\n throw new Error('Bad request! Some fields are missing or invalid!')\n }\n\n // 403 Forbidden\n if (response.status === 403) {\n throw new Error('Forbidden! Payload tag does not match the requested file!')\n }\n\n // 402 Payment Required\n if (response.status === 402) {\n throw new Error('Payment required!')\n }\n\n // unknown error\n throw new Error('Unknown error in uploading file!')\n }\n\n try {\n const parsedResponse = await response.json()\n\n if (!validateFileUploadResponse(parsedResponse)) {\n throw new Error('Invalid response from the server!')\n }\n\n return parsedResponse\n } catch (error) {\n throw new Error('Error parsing JSON response!')\n }\n}\n\n/**\n * Generates the URL for downloading a file from a NIP-96 compliant server.\n *\n * @param fileHash - The SHA-256 hash of the original file.\n * @param serverDownloadUrl - The base URL provided by the server, retrieved from the server's configuration.\n * @param fileExtension - An optional parameter that specifies the file extension (e.g., '.jpg', '.png').\n * @returns A string representing the complete URL to download the file.\n *\n */\nexport function generateDownloadUrl(fileHash: string, serverDownloadUrl: string, fileExtension?: string): string {\n // Construct the base download URL using the file hash\n let downloadUrl = `${serverDownloadUrl}/${fileHash}`\n\n // Append the file extension if provided\n if (fileExtension) {\n downloadUrl += fileExtension\n }\n\n return downloadUrl\n}\n\n/**\n * Sends a request to delete a file from a NIP-96 compliant server.\n *\n * @param fileHash - The SHA-256 hash of the original file.\n * @param serverApiUrl - The base API URL of the server, retrieved from the server's configuration.\n * @param nip98AuthorizationHeader - The authorization header from NIP-98.\n * @returns A promise that resolves to the server's response to the deletion request.\n *\n */\nexport async function deleteFile(\n fileHash: string,\n serverApiUrl: string,\n nip98AuthorizationHeader: string,\n): Promise<any> {\n // make sure the serverApiUrl ends with a slash\n if (!serverApiUrl.endsWith('/')) {\n serverApiUrl += '/'\n }\n\n // Construct the URL for the delete request\n const deleteUrl = `${serverApiUrl}${fileHash}`\n\n // Send the DELETE request\n const response = await fetch(deleteUrl, {\n method: 'DELETE',\n headers: {\n Authorization: nip98AuthorizationHeader,\n },\n })\n\n // Handle the response\n if (!response.ok) {\n throw new Error('Error deleting file!')\n }\n\n // Return the response from the server\n try {\n return await response.json()\n } catch (error) {\n throw new Error('Error parsing JSON response!')\n }\n}\n\n/**\n * Validates the server's response to a delayed processing request.\n *\n * @param response - The server's response to a delayed processing request.\n * @returns A boolean indicating whether the response is valid.\n */\nexport function validateDelayedProcessingResponse(response: any): response is DelayedProcessingResponse {\n if (typeof response !== 'object' || response === null) return false\n\n if (!response.status || !response.message || !response.percentage) {\n return false\n }\n\n if (response.status !== 'processing' && response.status !== 'error') {\n return false\n }\n\n if (typeof response.message !== 'string') {\n return false\n }\n\n if (typeof response.percentage !== 'number') {\n return false\n }\n\n if (Number(response.percentage) < 0 || Number(response.percentage) > 100) {\n return false\n }\n\n return true\n}\n\n/**\n * Checks the processing status of a file when delayed processing is used.\n *\n * @param processingUrl - The URL provided by the server where the processing status can be checked.\n * @returns A promise that resolves to an object containing the processing status and other relevant information.\n */\nexport async function checkFileProcessingStatus(\n processingUrl: string,\n): Promise<FileUploadResponse | DelayedProcessingResponse> {\n // Make the GET request to the processing URL\n const response = await fetch(processingUrl)\n\n // Handle the response\n if (!response.ok) {\n throw new Error(`Failed to retrieve processing status. Server responded with status: ${response.status}`)\n }\n\n // Parse the response\n try {\n const parsedResponse = await response.json()\n\n // 201 Created: Indicates the processing is over.\n if (response.status === 201) {\n // Validate the response\n if (!validateFileUploadResponse(parsedResponse)) {\n throw new Error('Invalid response from the server!')\n }\n\n return parsedResponse\n }\n\n // 200 OK: Indicates the processing is still ongoing.\n if (response.status === 200) {\n // Validate the response\n if (!validateDelayedProcessingResponse(parsedResponse)) {\n throw new Error('Invalid response from the server!')\n }\n\n return parsedResponse\n }\n\n throw new Error('Invalid response from the server!')\n } catch (error) {\n throw new Error('Error parsing JSON response!')\n }\n}\n\n/**\n * Generates an event template to indicate a user's File Server Preferences.\n * This event is of kind 10096 and is used to specify one or more preferred servers for file uploads.\n *\n * @param serverUrls - An array of URLs representing the user's preferred file storage servers.\n * @returns An object representing a Nostr event template for setting file server preferences.\n */\nexport function generateFSPEventTemplate(serverUrls: string[]): EventTemplate {\n serverUrls = serverUrls.filter(serverUrl => {\n try {\n new URL(serverUrl)\n return true\n } catch (error) {\n return false\n }\n })\n\n return {\n kind: FileServerPreference,\n content: '',\n tags: serverUrls.map(serverUrl => ['server', serverUrl]),\n created_at: Math.floor(Date.now() / 1000),\n }\n}\n\n/**\n * Calculates the SHA-256 hash of a given file. This hash is used in various NIP-96 operations,\n * such as file upload, download, and deletion, to uniquely identify files.\n *\n * @param file - The file for which the SHA-256 hash needs to be calculated.\n * @returns A promise that resolves to the SHA-256 hash of the file.\n */\nexport async function calculateFileHash(file: Blob): Promise<string> {\n // Read the file as an ArrayBuffer\n const buffer = await file.arrayBuffer()\n\n // Calculate the SHA-256 hash of the file\n const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)\n\n // Convert the hash to a hexadecimal string\n const hashArray = Array.from(new Uint8Array(hashBuffer))\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')\n\n return hashHex\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number) {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number) {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number) {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number) {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { sha256 } from '@noble/hashes/sha256'\nimport { EventTemplate } from './core'\nimport { FileServerPreference } from './kinds'\nimport { bytesToHex } from '@noble/hashes/utils'\n\n/**\n * Represents the configuration for a server compliant with NIP-96.\n */\nexport type ServerConfiguration = {\n /**\n * The base URL from which file upload and deletion operations are served.\n * Also used for downloads if \"download_url\" is not specified.\n */\n api_url: string\n\n /**\n * Optional. The base URL from which files are downloaded.\n * Used if different from the \"api_url\".\n */\n download_url?: string\n\n /**\n * Optional. URL of another HTTP file storage server's configuration.\n * Used by nostr relays to delegate to another server.\n * In this case, \"api_url\" must be an empty string.\n */\n delegated_to_url?: string\n\n /**\n * Optional. An array of NIP numbers that this server supports.\n */\n supported_nips?: number[]\n\n /**\n * Optional. URL to the server's Terms of Service.\n */\n tos_url?: string\n\n /**\n * Optional. An array of MIME types supported by the server.\n */\n content_types?: string[]\n\n /**\n * Optional. Defines various storage plans offered by the server.\n */\n plans?: {\n [planKey: string]: {\n /**\n * The name of the storage plan.\n */\n name: string\n\n /**\n * Optional. Indicates whether NIP-98 is required for uploads in this plan.\n */\n is_nip98_required?: boolean\n\n /**\n * Optional. URL to a landing page providing more information about the plan.\n */\n url?: string\n\n /**\n * Optional. The maximum file size allowed under this plan, in bytes.\n */\n max_byte_size?: number\n\n /**\n * Optional. Defines the range of file expiration in days.\n * The first value indicates the minimum expiration time, and the second value indicates the maximum.\n * A value of 0 indicates no expiration.\n */\n file_expiration?: [number, number]\n\n /**\n * Optional. Specifies the types of media transformations supported under this plan.\n * Currently, only image transformations are considered.\n */\n media_transformations?: {\n /**\n * Optional. An array of supported image transformation types.\n */\n image?: string[]\n }\n }\n }\n}\n\n/**\n * Represents the optional form data fields for file upload in accordance with NIP-96.\n */\nexport type OptionalFormDataFields = {\n /**\n * Specifies the desired expiration time of the file on the server.\n * It should be a string representing a UNIX timestamp in seconds.\n * An empty string indicates that the file should be stored indefinitely.\n */\n expiration?: string\n\n /**\n * Indicates the size of the file in bytes.\n * This field can be used by the server to pre-validate the file size before processing the upload.\n */\n size?: string\n\n /**\n * Provides a strict description of the file for accessibility purposes,\n * particularly useful for visibility-impaired users.\n */\n alt?: string\n\n /**\n * A loose, more descriptive caption for the file.\n * This can be used for additional context or commentary about the file.\n */\n caption?: string\n\n /**\n * Specifies the intended use of the file.\n * Can be either 'avatar' or 'banner', indicating if the file is to be used as an avatar or a banner.\n * Absence of this field suggests standard file upload without special treatment.\n */\n media_type?: 'avatar' | 'banner'\n\n /**\n * The MIME type of the file being uploaded.\n * This can be used for early rejection by the server if the file type isn't supported.\n */\n content_type?: string\n\n /**\n * Other custom form data fields.\n */\n [key: string]: string | undefined\n}\n\n/**\n * Type representing the response from a NIP-96 compliant server after a file upload request.\n */\nexport type FileUploadResponse = {\n /**\n * The status of the upload request.\n * - 'success': Indicates the file was successfully uploaded.\n * - 'error': Indicates there was an error in the upload process.\n * - 'processing': Indicates the file is still being processed (used in cases of delayed processing).\n */\n status: 'success' | 'error' | 'processing'\n\n /**\n * A message provided by the server, which could be a success message, error description, or processing status.\n */\n message: string\n\n /**\n * Optional. A URL provided by the server where the upload processing status can be checked.\n * This is relevant in cases where the file upload involves delayed processing.\n */\n processing_url?: string\n\n /**\n * Optional. An event object conforming to NIP-94, which includes details about the uploaded file.\n * This object is typically provided in the response for a successful upload and contains\n * essential information such as the download URL and file metadata.\n */\n nip94_event?: {\n /**\n * A collection of key-value pairs (tags) providing metadata about the uploaded file.\n * Standard tags include:\n * - 'url': The URL where the file can be accessed.\n * - 'ox': The SHA-256 hash of the original file before any server-side transformations.\n * Additional optional tags might include file dimensions, MIME type, etc.\n */\n tags: Array<[string, string]>\n\n /**\n * A content field, which is typically empty for file upload events but included for consistency with the NIP-94 structure.\n */\n content: string\n }\n}\n\n/**\n * Type representing the response from a NIP-96 compliant server after a delayed processing request.\n */\nexport type DelayedProcessingResponse = {\n /**\n * The status of the delayed processing request.\n * - 'processing': Indicates the file is still being processed.\n * - 'error': Indicates there was an error in the processing.\n */\n status: 'processing' | 'error'\n\n /**\n * A message provided by the server, which could be a success message or error description.\n */\n message: string\n\n /**\n * The percentage of the file that has been processed. This is a number between 0 and 100.\n */\n percentage: number\n}\n\n/**\n * Validates the server configuration.\n *\n * @param config - The server configuration object.\n * @returns True if the configuration is valid, false otherwise.\n */\nexport function validateServerConfiguration(config: ServerConfiguration): boolean {\n if (Boolean(config.api_url) == false) {\n return false\n }\n\n if (Boolean(config.delegated_to_url) && Boolean(config.api_url)) {\n return false\n }\n\n return true\n}\n\n/**\n * Fetches, parses, and validates the server configuration from the given URL.\n *\n * @param serverUrl The URL of the server.\n * @returns The server configuration, or an error if the configuration could not be fetched or parsed.\n */\nexport async function readServerConfig(serverUrl: string): Promise<ServerConfiguration> {\n const HTTPROUTE = '/.well-known/nostr/nip96.json' as const\n let fetchUrl = ''\n\n try {\n const { origin } = new URL(serverUrl)\n fetchUrl = origin + HTTPROUTE\n } catch (error) {\n throw new Error('Invalid URL')\n }\n\n try {\n const response = await fetch(fetchUrl)\n\n if (!response.ok) {\n throw new Error(`Error fetching ${fetchUrl}: ${response.statusText}`)\n }\n\n const data: any = await response.json()\n\n if (!data) {\n throw new Error('No data')\n }\n\n if (!validateServerConfiguration(data)) {\n throw new Error('Invalid configuration data')\n }\n\n return data\n } catch (_) {\n throw new Error(`Error fetching.`)\n }\n}\n\n/**\n * Validates if the given object is a valid FileUploadResponse.\n *\n * @param response - The object to validate.\n * @returns true if the object is a valid FileUploadResponse, otherwise false.\n */\nexport function validateFileUploadResponse(response: any): response is FileUploadResponse {\n if (typeof response !== 'object' || response === null) return false\n\n if (!response.status || !response.message) {\n return false\n }\n\n if (response.status !== 'success' && response.status !== 'error' && response.status !== 'processing') {\n return false\n }\n\n if (typeof response.message !== 'string') {\n return false\n }\n\n if (response.status === 'processing' && !response.processing_url) {\n return false\n }\n\n if (response.processing_url) {\n if (typeof response.processing_url !== 'string') {\n return false\n }\n }\n\n if (response.status === 'success' && !response.nip94_event) {\n return false\n }\n\n if (response.nip94_event) {\n if (\n !response.nip94_event.tags ||\n !Array.isArray(response.nip94_event.tags) ||\n response.nip94_event.tags.length === 0\n ) {\n return false\n }\n\n for (const tag of response.nip94_event.tags) {\n if (!Array.isArray(tag) || tag.length !== 2) return false\n\n if (typeof tag[0] !== 'string' || typeof tag[1] !== 'string') return false\n }\n\n if (!(response.nip94_event.tags as string[]).find(t => t[0] === 'url')) {\n return false\n }\n\n if (!(response.nip94_event.tags as string[]).find(t => t[0] === 'ox')) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Uploads a file to a NIP-96 compliant server.\n *\n * @param file - The file to be uploaded.\n * @param serverApiUrl - The API URL of the server, retrieved from the server's configuration.\n * @param nip98AuthorizationHeader - The authorization header from NIP-98.\n * @param optionalFormDataFields - Optional form data fields.\n * @returns A promise that resolves to the server's response.\n */\nexport async function uploadFile(\n file: File,\n serverApiUrl: string,\n nip98AuthorizationHeader: string,\n optionalFormDataFields?: OptionalFormDataFields,\n): Promise<FileUploadResponse> {\n // Create FormData object\n const formData = new FormData()\n\n // Append the authorization header to HTML Form Data\n formData.append('Authorization', nip98AuthorizationHeader)\n\n // Append optional fields to FormData\n optionalFormDataFields &&\n Object.entries(optionalFormDataFields).forEach(([key, value]) => {\n if (value) {\n formData.append(key, value)\n }\n })\n\n // Append the file to FormData as the last field\n formData.append('file', file)\n\n // Make the POST request to the server\n const response = await fetch(serverApiUrl, {\n method: 'POST',\n headers: {\n Authorization: nip98AuthorizationHeader,\n 'Content-Type': 'multipart/form-data',\n },\n body: formData,\n })\n\n if (response.ok === false) {\n // 413 Payload Too Large\n if (response.status === 413) {\n throw new Error('File too large!')\n }\n\n // 400 Bad Request\n if (response.status === 400) {\n throw new Error('Bad request! Some fields are missing or invalid!')\n }\n\n // 403 Forbidden\n if (response.status === 403) {\n throw new Error('Forbidden! Payload tag does not match the requested file!')\n }\n\n // 402 Payment Required\n if (response.status === 402) {\n throw new Error('Payment required!')\n }\n\n // unknown error\n throw new Error('Unknown error in uploading file!')\n }\n\n try {\n const parsedResponse = await response.json()\n\n if (!validateFileUploadResponse(parsedResponse)) {\n throw new Error('Invalid response from the server!')\n }\n\n return parsedResponse\n } catch (error) {\n throw new Error('Error parsing JSON response!')\n }\n}\n\n/**\n * Generates the URL for downloading a file from a NIP-96 compliant server.\n *\n * @param fileHash - The SHA-256 hash of the original file.\n * @param serverDownloadUrl - The base URL provided by the server, retrieved from the server's configuration.\n * @param fileExtension - An optional parameter that specifies the file extension (e.g., '.jpg', '.png').\n * @returns A string representing the complete URL to download the file.\n *\n */\nexport function generateDownloadUrl(fileHash: string, serverDownloadUrl: string, fileExtension?: string): string {\n // Construct the base download URL using the file hash\n let downloadUrl = `${serverDownloadUrl}/${fileHash}`\n\n // Append the file extension if provided\n if (fileExtension) {\n downloadUrl += fileExtension\n }\n\n return downloadUrl\n}\n\n/**\n * Sends a request to delete a file from a NIP-96 compliant server.\n *\n * @param fileHash - The SHA-256 hash of the original file.\n * @param serverApiUrl - The base API URL of the server, retrieved from the server's configuration.\n * @param nip98AuthorizationHeader - The authorization header from NIP-98.\n * @returns A promise that resolves to the server's response to the deletion request.\n *\n */\nexport async function deleteFile(\n fileHash: string,\n serverApiUrl: string,\n nip98AuthorizationHeader: string,\n): Promise<any> {\n // make sure the serverApiUrl ends with a slash\n if (!serverApiUrl.endsWith('/')) {\n serverApiUrl += '/'\n }\n\n // Construct the URL for the delete request\n const deleteUrl = `${serverApiUrl}${fileHash}`\n\n // Send the DELETE request\n const response = await fetch(deleteUrl, {\n method: 'DELETE',\n headers: {\n Authorization: nip98AuthorizationHeader,\n },\n })\n\n // Handle the response\n if (!response.ok) {\n throw new Error('Error deleting file!')\n }\n\n // Return the response from the server\n try {\n return await response.json()\n } catch (error) {\n throw new Error('Error parsing JSON response!')\n }\n}\n\n/**\n * Validates the server's response to a delayed processing request.\n *\n * @param response - The server's response to a delayed processing request.\n * @returns A boolean indicating whether the response is valid.\n */\nexport function validateDelayedProcessingResponse(response: any): response is DelayedProcessingResponse {\n if (typeof response !== 'object' || response === null) return false\n\n if (!response.status || !response.message || !response.percentage) {\n return false\n }\n\n if (response.status !== 'processing' && response.status !== 'error') {\n return false\n }\n\n if (typeof response.message !== 'string') {\n return false\n }\n\n if (typeof response.percentage !== 'number') {\n return false\n }\n\n if (Number(response.percentage) < 0 || Number(response.percentage) > 100) {\n return false\n }\n\n return true\n}\n\n/**\n * Checks the processing status of a file when delayed processing is used.\n *\n * @param processingUrl - The URL provided by the server where the processing status can be checked.\n * @returns A promise that resolves to an object containing the processing status and other relevant information.\n */\nexport async function checkFileProcessingStatus(\n processingUrl: string,\n): Promise<FileUploadResponse | DelayedProcessingResponse> {\n // Make the GET request to the processing URL\n const response = await fetch(processingUrl)\n\n // Handle the response\n if (!response.ok) {\n throw new Error(`Failed to retrieve processing status. Server responded with status: ${response.status}`)\n }\n\n // Parse the response\n try {\n const parsedResponse = await response.json()\n\n // 201 Created: Indicates the processing is over.\n if (response.status === 201) {\n // Validate the response\n if (!validateFileUploadResponse(parsedResponse)) {\n throw new Error('Invalid response from the server!')\n }\n\n return parsedResponse\n }\n\n // 200 OK: Indicates the processing is still ongoing.\n if (response.status === 200) {\n // Validate the response\n if (!validateDelayedProcessingResponse(parsedResponse)) {\n throw new Error('Invalid response from the server!')\n }\n\n return parsedResponse\n }\n\n throw new Error('Invalid response from the server!')\n } catch (error) {\n throw new Error('Error parsing JSON response!')\n }\n}\n\n/**\n * Generates an event template to indicate a user's File Server Preferences.\n * This event is of kind 10096 and is used to specify one or more preferred servers for file uploads.\n *\n * @param serverUrls - An array of URLs representing the user's preferred file storage servers.\n * @returns An object representing a Nostr event template for setting file server preferences.\n */\nexport function generateFSPEventTemplate(serverUrls: string[]): EventTemplate {\n serverUrls = serverUrls.filter(serverUrl => {\n try {\n new URL(serverUrl)\n return true\n } catch (error) {\n return false\n }\n })\n\n return {\n kind: FileServerPreference,\n content: '',\n tags: serverUrls.map(serverUrl => ['server', serverUrl]),\n created_at: Math.floor(Date.now() / 1000),\n }\n}\n\n/**\n * Calculates the SHA-256 hash of a given file. This hash is used in various NIP-96 operations,\n * such as file upload, download, and deletion, to uniquely identify files.\n *\n * @param file - The file for which the SHA-256 hash needs to be calculated.\n * @returns A promise that resolves to the SHA-256 hash of the file.\n */\nexport async function calculateFileHash(file: Blob): Promise<string> {\n return bytesToHex(sha256(new Uint8Array(await file.arrayBuffer())))\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number): boolean {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number): boolean {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number): boolean {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number): boolean {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../nip98.ts", "../../kinds.ts", "../../pure.ts", "../../core.ts", "../../utils.ts"],
4
- "sourcesContent": ["import { sha256 } from '@noble/hashes/sha256'\nimport { bytesToHex } from '@noble/hashes/utils'\nimport { base64 } from '@scure/base'\n\nimport { HTTPAuth } from './kinds.ts'\nimport { Event, EventTemplate, verifyEvent } from './pure.ts'\nimport { utf8Decoder, utf8Encoder } from './utils.ts'\n\nconst _authorizationScheme = 'Nostr '\n\n/**\n * Generate token for NIP-98 flow.\n *\n * @example\n * const sign = window.nostr.signEvent\n * await nip98.getToken('https://example.com/login', 'post', (e) => sign(e), true)\n */\nexport async function getToken(\n loginUrl: string,\n httpMethod: string,\n sign: (e: EventTemplate) => Promise<Event> | Event,\n includeAuthorizationScheme: boolean = false,\n payload?: Record<string, any>,\n): Promise<string> {\n const event: EventTemplate = {\n kind: HTTPAuth,\n tags: [\n ['u', loginUrl],\n ['method', httpMethod],\n ],\n created_at: Math.round(new Date().getTime() / 1000),\n content: '',\n }\n\n if (payload) {\n event.tags.push(['payload', hashPayload(payload)])\n }\n\n const signedEvent = await sign(event)\n const authorizationScheme = includeAuthorizationScheme ? _authorizationScheme : ''\n\n return authorizationScheme + base64.encode(utf8Encoder.encode(JSON.stringify(signedEvent)))\n}\n\n/**\n * Validate token for NIP-98 flow.\n *\n * @example\n * await nip98.validateToken('Nostr base64token', 'https://example.com/login', 'post')\n */\nexport async function validateToken(token: string, url: string, method: string): Promise<boolean> {\n const event = await unpackEventFromToken(token).catch(error => {\n throw error\n })\n\n const valid = await validateEvent(event, url, method).catch(error => {\n throw error\n })\n\n return valid\n}\n\n/**\n * Unpacks an event from a token.\n *\n * @param token - The token to unpack.\n * @returns A promise that resolves to the unpacked event.\n * @throws {Error} If the token is missing, invalid, or cannot be parsed.\n */\nexport async function unpackEventFromToken(token: string): Promise<Event> {\n if (!token) {\n throw new Error('Missing token')\n }\n\n token = token.replace(_authorizationScheme, '')\n\n const eventB64 = utf8Decoder.decode(base64.decode(token))\n if (!eventB64 || eventB64.length === 0 || !eventB64.startsWith('{')) {\n throw new Error('Invalid token')\n }\n\n const event = JSON.parse(eventB64) as Event\n\n return event\n}\n\n/**\n * Validates the timestamp of an event.\n * @param event - The event object to validate.\n * @returns A boolean indicating whether the event timestamp is within the last 60 seconds.\n */\nexport function validateEventTimestamp(event: Event): boolean {\n if (!event.created_at) {\n return false\n }\n\n return Math.round(new Date().getTime() / 1000) - event.created_at < 60\n}\n\n/**\n * Validates the kind of an event.\n * @param event The event to validate.\n * @returns A boolean indicating whether the event kind is valid.\n */\nexport function validateEventKind(event: Event): boolean {\n return event.kind === HTTPAuth\n}\n\n/**\n * Validates if the given URL matches the URL tag of the event.\n * @param event - The event object.\n * @param url - The URL to validate.\n * @returns A boolean indicating whether the URL is valid or not.\n */\nexport function validateEventUrlTag(event: Event, url: string): boolean {\n const urlTag = event.tags.find(t => t[0] === 'u')\n\n if (!urlTag) {\n return false\n }\n\n return urlTag.length > 0 && urlTag[1] === url\n}\n\n/**\n * Validates if the given event has a method tag that matches the specified method.\n * @param event - The event to validate.\n * @param method - The method to match against the method tag.\n * @returns A boolean indicating whether the event has a matching method tag.\n */\nexport function validateEventMethodTag(event: Event, method: string): boolean {\n const methodTag = event.tags.find(t => t[0] === 'method')\n\n if (!methodTag) {\n return false\n }\n\n return methodTag.length > 0 && methodTag[1].toLowerCase() === method.toLowerCase()\n}\n\n/**\n * Calculates the hash of a payload.\n * @param payload - The payload to be hashed.\n * @returns The hash value as a string.\n */\nexport function hashPayload(payload: any): string {\n const hash = sha256(utf8Encoder.encode(JSON.stringify(payload)))\n return bytesToHex(hash)\n}\n\n/**\n * Validates the event payload tag against the provided payload.\n * @param event The event object.\n * @param payload The payload to validate.\n * @returns A boolean indicating whether the payload tag is valid.\n */\nexport function validateEventPayloadTag(event: Event, payload: any): boolean {\n const payloadTag = event.tags.find(t => t[0] === 'payload')\n\n if (!payloadTag) {\n return false\n }\n\n const payloadHash = hashPayload(payload)\n return payloadTag.length > 0 && payloadTag[1] === payloadHash\n}\n\n/**\n * Validates a Nostr event for the NIP-98 flow.\n *\n * @param event - The Nostr event to validate.\n * @param url - The URL associated with the event.\n * @param method - The HTTP method associated with the event.\n * @param body - The request body associated with the event (optional).\n * @returns A promise that resolves to a boolean indicating whether the event is valid.\n * @throws An error if the event is invalid.\n */\nexport async function validateEvent(event: Event, url: string, method: string, body?: any): Promise<boolean> {\n if (!verifyEvent(event)) {\n throw new Error('Invalid nostr event, signature invalid')\n }\n\n if (!validateEventKind(event)) {\n throw new Error('Invalid nostr event, kind invalid')\n }\n\n if (!validateEventTimestamp(event)) {\n throw new Error('Invalid nostr event, created_at timestamp invalid')\n }\n\n if (!validateEventUrlTag(event, url)) {\n throw new Error('Invalid nostr event, url tag invalid')\n }\n\n if (!validateEventMethodTag(event, method)) {\n throw new Error('Invalid nostr event, method tag invalid')\n }\n\n if (Boolean(body) && typeof body === 'object' && Object.keys(body).length > 0) {\n if (!validateEventPayloadTag(event, body)) {\n throw new Error('Invalid nostr event, payload tag does not match request body hash')\n }\n }\n\n return true\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number) {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number) {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number) {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number) {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n", "import { schnorr } from '@noble/curves/secp256k1'\nimport { bytesToHex } from '@noble/hashes/utils'\nimport { Nostr, Event, EventTemplate, UnsignedEvent, VerifiedEvent, verifiedSymbol, validateEvent } from './core.ts'\nimport { sha256 } from '@noble/hashes/sha256'\n\nimport { utf8Encoder } from './utils.ts'\n\nclass JS implements Nostr {\n generateSecretKey(): Uint8Array {\n return schnorr.utils.randomPrivateKey()\n }\n getPublicKey(secretKey: Uint8Array): string {\n return bytesToHex(schnorr.getPublicKey(secretKey))\n }\n finalizeEvent(t: EventTemplate, secretKey: Uint8Array): VerifiedEvent {\n const event = t as VerifiedEvent\n event.pubkey = bytesToHex(schnorr.getPublicKey(secretKey))\n event.id = getEventHash(event)\n event.sig = bytesToHex(schnorr.sign(getEventHash(event), secretKey))\n event[verifiedSymbol] = true\n return event\n }\n verifyEvent(event: Event): event is VerifiedEvent {\n if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol]\n\n const hash = getEventHash(event)\n if (hash !== event.id) {\n event[verifiedSymbol] = false\n return false\n }\n\n try {\n const valid = schnorr.verify(event.sig, hash, event.pubkey)\n event[verifiedSymbol] = valid\n return valid\n } catch (err) {\n event[verifiedSymbol] = false\n return false\n }\n }\n}\n\nexport function serializeEvent(evt: UnsignedEvent): string {\n if (!validateEvent(evt)) throw new Error(\"can't serialize event with wrong or missing properties\")\n return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content])\n}\n\nexport function getEventHash(event: UnsignedEvent): string {\n let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)))\n return bytesToHex(eventHash)\n}\n\nconst i = new JS()\n\nexport const generateSecretKey = i.generateSecretKey\nexport const getPublicKey = i.getPublicKey\nexport const finalizeEvent = i.finalizeEvent\nexport const verifyEvent = i.verifyEvent\nexport * from './core.ts'\n", "export interface Nostr {\n generateSecretKey(): Uint8Array\n getPublicKey(secretKey: Uint8Array): string\n finalizeEvent(event: EventTemplate, secretKey: Uint8Array): VerifiedEvent\n verifyEvent(event: Event): event is VerifiedEvent\n}\n\n/** Designates a verified event signature. */\nexport const verifiedSymbol = Symbol('verified')\n\nexport interface Event {\n kind: number\n tags: string[][]\n content: string\n created_at: number\n pubkey: string\n id: string\n sig: string\n [verifiedSymbol]?: boolean\n}\n\nexport type NostrEvent = Event\nexport type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>\nexport type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>\n\n/** An event whose signature has been verified. */\nexport interface VerifiedEvent extends Event {\n [verifiedSymbol]: true\n}\n\nconst isRecord = (obj: unknown): obj is Record<string, unknown> => obj instanceof Object\n\nexport function validateEvent<T>(event: T): event is T & UnsignedEvent {\n if (!isRecord(event)) return false\n if (typeof event.kind !== 'number') return false\n if (typeof event.content !== 'string') return false\n if (typeof event.created_at !== 'number') return false\n if (typeof event.pubkey !== 'string') return false\n if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false\n\n if (!Array.isArray(event.tags)) return false\n for (let i = 0; i < event.tags.length; i++) {\n let tag = event.tags[i]\n if (!Array.isArray(tag)) return false\n for (let j = 0; j < tag.length; j++) {\n if (typeof tag[j] === 'object') return false\n }\n }\n\n return true\n}\n", "import type { Event } from './core.ts'\n\nexport const utf8Decoder = new TextDecoder('utf-8')\nexport const utf8Encoder = new TextEncoder()\n\nexport function normalizeURL(url: string): string {\n if (url.indexOf('://') === -1) url = 'wss://' + url\n let p = new URL(url)\n p.pathname = p.pathname.replace(/\\/+/g, '/')\n if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)\n if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) p.port = ''\n p.searchParams.sort()\n p.hash = ''\n return p.toString()\n}\n\nexport function insertEventIntoDescendingList(sortedArray: Event[], event: Event) {\n const [idx, found] = binarySearch(sortedArray, b => {\n if (event.id === b.id) return 0\n if (event.created_at === b.created_at) return -1\n return b.created_at - event.created_at\n })\n if (!found) {\n sortedArray.splice(idx, 0, event)\n }\n return sortedArray\n}\n\nexport function insertEventIntoAscendingList(sortedArray: Event[], event: Event) {\n const [idx, found] = binarySearch(sortedArray, b => {\n if (event.id === b.id) return 0\n if (event.created_at === b.created_at) return -1\n return event.created_at - b.created_at\n })\n if (!found) {\n sortedArray.splice(idx, 0, event)\n }\n return sortedArray\n}\n\nexport function binarySearch<T>(arr: T[], compare: (b: T) => number): [number, boolean] {\n let start = 0\n let end = arr.length - 1\n\n while (start <= end) {\n const mid = Math.floor((start + end) / 2)\n const cmp = compare(arr[mid])\n\n if (cmp === 0) {\n return [mid, true]\n }\n\n if (cmp < 0) {\n end = mid - 1\n } else {\n start = mid + 1\n }\n }\n\n return [start, false]\n}\n\nexport class QueueNode<V> {\n public value: V\n public next: QueueNode<V> | null = null\n public prev: QueueNode<V> | null = null\n\n constructor(message: V) {\n this.value = message\n }\n}\n\nexport class Queue<V> {\n public first: QueueNode<V> | null\n public last: QueueNode<V> | null\n\n constructor() {\n this.first = null\n this.last = null\n }\n\n enqueue(value: V): boolean {\n const newNode = new QueueNode(value)\n if (!this.last) {\n // list is empty\n this.first = newNode\n this.last = newNode\n } else if (this.last === this.first) {\n // list has a single element\n this.last = newNode\n this.last.prev = this.first\n this.first.next = newNode\n } else {\n // list has elements, add as last\n newNode.prev = this.last\n this.last.next = newNode\n this.last = newNode\n }\n return true\n }\n\n dequeue(): V | null {\n if (!this.first) return null\n\n if (this.first === this.last) {\n const target = this.first\n this.first = null\n this.last = null\n return target.value\n }\n\n const target = this.first\n this.first = target.next\n\n return target.value\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { sha256 } from '@noble/hashes/sha256'\nimport { bytesToHex } from '@noble/hashes/utils'\nimport { base64 } from '@scure/base'\n\nimport { HTTPAuth } from './kinds.ts'\nimport { Event, EventTemplate, verifyEvent } from './pure.ts'\nimport { utf8Decoder, utf8Encoder } from './utils.ts'\n\nconst _authorizationScheme = 'Nostr '\n\n/**\n * Generate token for NIP-98 flow.\n *\n * @example\n * const sign = window.nostr.signEvent\n * await nip98.getToken('https://example.com/login', 'post', (e) => sign(e), true)\n */\nexport async function getToken(\n loginUrl: string,\n httpMethod: string,\n sign: (e: EventTemplate) => Promise<Event> | Event,\n includeAuthorizationScheme: boolean = false,\n payload?: Record<string, any>,\n): Promise<string> {\n const event: EventTemplate = {\n kind: HTTPAuth,\n tags: [\n ['u', loginUrl],\n ['method', httpMethod],\n ],\n created_at: Math.round(new Date().getTime() / 1000),\n content: '',\n }\n\n if (payload) {\n event.tags.push(['payload', hashPayload(payload)])\n }\n\n const signedEvent = await sign(event)\n const authorizationScheme = includeAuthorizationScheme ? _authorizationScheme : ''\n\n return authorizationScheme + base64.encode(utf8Encoder.encode(JSON.stringify(signedEvent)))\n}\n\n/**\n * Validate token for NIP-98 flow.\n *\n * @example\n * await nip98.validateToken('Nostr base64token', 'https://example.com/login', 'post')\n */\nexport async function validateToken(token: string, url: string, method: string): Promise<boolean> {\n const event = await unpackEventFromToken(token).catch(error => {\n throw error\n })\n\n const valid = await validateEvent(event, url, method).catch(error => {\n throw error\n })\n\n return valid\n}\n\n/**\n * Unpacks an event from a token.\n *\n * @param token - The token to unpack.\n * @returns A promise that resolves to the unpacked event.\n * @throws {Error} If the token is missing, invalid, or cannot be parsed.\n */\nexport async function unpackEventFromToken(token: string): Promise<Event> {\n if (!token) {\n throw new Error('Missing token')\n }\n\n token = token.replace(_authorizationScheme, '')\n\n const eventB64 = utf8Decoder.decode(base64.decode(token))\n if (!eventB64 || eventB64.length === 0 || !eventB64.startsWith('{')) {\n throw new Error('Invalid token')\n }\n\n const event = JSON.parse(eventB64) as Event\n\n return event\n}\n\n/**\n * Validates the timestamp of an event.\n * @param event - The event object to validate.\n * @returns A boolean indicating whether the event timestamp is within the last 60 seconds.\n */\nexport function validateEventTimestamp(event: Event): boolean {\n if (!event.created_at) {\n return false\n }\n\n return Math.round(new Date().getTime() / 1000) - event.created_at < 60\n}\n\n/**\n * Validates the kind of an event.\n * @param event The event to validate.\n * @returns A boolean indicating whether the event kind is valid.\n */\nexport function validateEventKind(event: Event): boolean {\n return event.kind === HTTPAuth\n}\n\n/**\n * Validates if the given URL matches the URL tag of the event.\n * @param event - The event object.\n * @param url - The URL to validate.\n * @returns A boolean indicating whether the URL is valid or not.\n */\nexport function validateEventUrlTag(event: Event, url: string): boolean {\n const urlTag = event.tags.find(t => t[0] === 'u')\n\n if (!urlTag) {\n return false\n }\n\n return urlTag.length > 0 && urlTag[1] === url\n}\n\n/**\n * Validates if the given event has a method tag that matches the specified method.\n * @param event - The event to validate.\n * @param method - The method to match against the method tag.\n * @returns A boolean indicating whether the event has a matching method tag.\n */\nexport function validateEventMethodTag(event: Event, method: string): boolean {\n const methodTag = event.tags.find(t => t[0] === 'method')\n\n if (!methodTag) {\n return false\n }\n\n return methodTag.length > 0 && methodTag[1].toLowerCase() === method.toLowerCase()\n}\n\n/**\n * Calculates the hash of a payload.\n * @param payload - The payload to be hashed.\n * @returns The hash value as a string.\n */\nexport function hashPayload(payload: any): string {\n const hash = sha256(utf8Encoder.encode(JSON.stringify(payload)))\n return bytesToHex(hash)\n}\n\n/**\n * Validates the event payload tag against the provided payload.\n * @param event The event object.\n * @param payload The payload to validate.\n * @returns A boolean indicating whether the payload tag is valid.\n */\nexport function validateEventPayloadTag(event: Event, payload: any): boolean {\n const payloadTag = event.tags.find(t => t[0] === 'payload')\n\n if (!payloadTag) {\n return false\n }\n\n const payloadHash = hashPayload(payload)\n return payloadTag.length > 0 && payloadTag[1] === payloadHash\n}\n\n/**\n * Validates a Nostr event for the NIP-98 flow.\n *\n * @param event - The Nostr event to validate.\n * @param url - The URL associated with the event.\n * @param method - The HTTP method associated with the event.\n * @param body - The request body associated with the event (optional).\n * @returns A promise that resolves to a boolean indicating whether the event is valid.\n * @throws An error if the event is invalid.\n */\nexport async function validateEvent(event: Event, url: string, method: string, body?: any): Promise<boolean> {\n if (!verifyEvent(event)) {\n throw new Error('Invalid nostr event, signature invalid')\n }\n\n if (!validateEventKind(event)) {\n throw new Error('Invalid nostr event, kind invalid')\n }\n\n if (!validateEventTimestamp(event)) {\n throw new Error('Invalid nostr event, created_at timestamp invalid')\n }\n\n if (!validateEventUrlTag(event, url)) {\n throw new Error('Invalid nostr event, url tag invalid')\n }\n\n if (!validateEventMethodTag(event, method)) {\n throw new Error('Invalid nostr event, method tag invalid')\n }\n\n if (Boolean(body) && typeof body === 'object' && Object.keys(body).length > 0) {\n if (!validateEventPayloadTag(event, body)) {\n throw new Error('Invalid nostr event, payload tag does not match request body hash')\n }\n }\n\n return true\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number): boolean {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number): boolean {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number): boolean {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number): boolean {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n", "import { schnorr } from '@noble/curves/secp256k1'\nimport { bytesToHex } from '@noble/hashes/utils'\nimport { Nostr, Event, EventTemplate, UnsignedEvent, VerifiedEvent, verifiedSymbol, validateEvent } from './core.ts'\nimport { sha256 } from '@noble/hashes/sha256'\n\nimport { utf8Encoder } from './utils.ts'\n\nclass JS implements Nostr {\n generateSecretKey(): Uint8Array {\n return schnorr.utils.randomPrivateKey()\n }\n getPublicKey(secretKey: Uint8Array): string {\n return bytesToHex(schnorr.getPublicKey(secretKey))\n }\n finalizeEvent(t: EventTemplate, secretKey: Uint8Array): VerifiedEvent {\n const event = t as VerifiedEvent\n event.pubkey = bytesToHex(schnorr.getPublicKey(secretKey))\n event.id = getEventHash(event)\n event.sig = bytesToHex(schnorr.sign(getEventHash(event), secretKey))\n event[verifiedSymbol] = true\n return event\n }\n verifyEvent(event: Event): event is VerifiedEvent {\n if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol]\n\n const hash = getEventHash(event)\n if (hash !== event.id) {\n event[verifiedSymbol] = false\n return false\n }\n\n try {\n const valid = schnorr.verify(event.sig, hash, event.pubkey)\n event[verifiedSymbol] = valid\n return valid\n } catch (err) {\n event[verifiedSymbol] = false\n return false\n }\n }\n}\n\nexport function serializeEvent(evt: UnsignedEvent): string {\n if (!validateEvent(evt)) throw new Error(\"can't serialize event with wrong or missing properties\")\n return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content])\n}\n\nexport function getEventHash(event: UnsignedEvent): string {\n let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)))\n return bytesToHex(eventHash)\n}\n\nconst i: JS = new JS()\n\nexport const generateSecretKey = i.generateSecretKey\nexport const getPublicKey = i.getPublicKey\nexport const finalizeEvent = i.finalizeEvent\nexport const verifyEvent = i.verifyEvent\nexport * from './core.ts'\n", "export interface Nostr {\n generateSecretKey(): Uint8Array\n getPublicKey(secretKey: Uint8Array): string\n finalizeEvent(event: EventTemplate, secretKey: Uint8Array): VerifiedEvent\n verifyEvent(event: Event): event is VerifiedEvent\n}\n\n/** Designates a verified event signature. */\nexport const verifiedSymbol = Symbol('verified')\n\nexport interface Event {\n kind: number\n tags: string[][]\n content: string\n created_at: number\n pubkey: string\n id: string\n sig: string\n [verifiedSymbol]?: boolean\n}\n\nexport type NostrEvent = Event\nexport type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>\nexport type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>\n\n/** An event whose signature has been verified. */\nexport interface VerifiedEvent extends Event {\n [verifiedSymbol]: true\n}\n\nconst isRecord = (obj: unknown): obj is Record<string, unknown> => obj instanceof Object\n\nexport function validateEvent<T>(event: T): event is T & UnsignedEvent {\n if (!isRecord(event)) return false\n if (typeof event.kind !== 'number') return false\n if (typeof event.content !== 'string') return false\n if (typeof event.created_at !== 'number') return false\n if (typeof event.pubkey !== 'string') return false\n if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false\n\n if (!Array.isArray(event.tags)) return false\n for (let i = 0; i < event.tags.length; i++) {\n let tag = event.tags[i]\n if (!Array.isArray(tag)) return false\n for (let j = 0; j < tag.length; j++) {\n if (typeof tag[j] === 'object') return false\n }\n }\n\n return true\n}\n", "import type { Event } from './core.ts'\n\nexport const utf8Decoder: TextDecoder = new TextDecoder('utf-8')\nexport const utf8Encoder: TextEncoder = new TextEncoder()\n\nexport function normalizeURL(url: string): string {\n if (url.indexOf('://') === -1) url = 'wss://' + url\n let p = new URL(url)\n p.pathname = p.pathname.replace(/\\/+/g, '/')\n if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)\n if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) p.port = ''\n p.searchParams.sort()\n p.hash = ''\n return p.toString()\n}\n\nexport function insertEventIntoDescendingList(sortedArray: Event[], event: Event): Event[] {\n const [idx, found] = binarySearch(sortedArray, b => {\n if (event.id === b.id) return 0\n if (event.created_at === b.created_at) return -1\n return b.created_at - event.created_at\n })\n if (!found) {\n sortedArray.splice(idx, 0, event)\n }\n return sortedArray\n}\n\nexport function insertEventIntoAscendingList(sortedArray: Event[], event: Event): Event[] {\n const [idx, found] = binarySearch(sortedArray, b => {\n if (event.id === b.id) return 0\n if (event.created_at === b.created_at) return -1\n return event.created_at - b.created_at\n })\n if (!found) {\n sortedArray.splice(idx, 0, event)\n }\n return sortedArray\n}\n\nexport function binarySearch<T>(arr: T[], compare: (b: T) => number): [number, boolean] {\n let start = 0\n let end = arr.length - 1\n\n while (start <= end) {\n const mid = Math.floor((start + end) / 2)\n const cmp = compare(arr[mid])\n\n if (cmp === 0) {\n return [mid, true]\n }\n\n if (cmp < 0) {\n end = mid - 1\n } else {\n start = mid + 1\n }\n }\n\n return [start, false]\n}\n\nexport class QueueNode<V> {\n public value: V\n public next: QueueNode<V> | null = null\n public prev: QueueNode<V> | null = null\n\n constructor(message: V) {\n this.value = message\n }\n}\n\nexport class Queue<V> {\n public first: QueueNode<V> | null\n public last: QueueNode<V> | null\n\n constructor() {\n this.first = null\n this.last = null\n }\n\n enqueue(value: V): boolean {\n const newNode = new QueueNode(value)\n if (!this.last) {\n // list is empty\n this.first = newNode\n this.last = newNode\n } else if (this.last === this.first) {\n // list has a single element\n this.last = newNode\n this.last.prev = this.first\n this.first.next = newNode\n } else {\n // list has elements, add as last\n newNode.prev = this.last\n this.last.next = newNode\n this.last = newNode\n }\n return true\n }\n\n dequeue(): V | null {\n if (!this.first) return null\n\n if (this.first === this.last) {\n const target = this.first\n this.first = null\n this.last = null\n return target.value\n }\n\n const target = this.first\n this.first = target.next\n\n return target.value\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["validateEvent", "import_sha256", "import_utils", "i", "validateEvent"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../nip99.ts", "../../kinds.ts"],
4
- "sourcesContent": ["import { Event, EventTemplate } from './core.ts'\nimport { ClassifiedListing, DraftClassifiedListing } from './kinds.ts'\n\n/**\n * Represents the details of a price.\n * @example { amount: '100', currency: 'USD', frequency: 'month' }\n * @example { amount: '100', currency: 'EUR' }\n */\nexport type PriceDetails = {\n /**\n * The amount of the price.\n */\n amount: string\n /**\n * The currency of the price in 3-letter ISO 4217 format.\n * @example 'USD'\n */\n currency: string\n /**\n * The optional frequency of payment.\n * Can be one of: 'hour', 'day', 'week', 'month', 'year', or a custom string.\n */\n frequency?: string\n}\n\n/**\n * Represents a classified listing object.\n */\nexport type ClassifiedListingObject = {\n /**\n * Whether the listing is a draft or not.\n */\n isDraft: boolean\n /**\n * A title of the listing.\n */\n title: string\n /**\n * A short summary or tagline.\n */\n summary: string\n /**\n * A description in Markdown format.\n */\n content: string\n /**\n * Timestamp in unix seconds of when the listing was published.\n */\n publishedAt: string\n /**\n * Location of the listing.\n * @example 'NYC'\n */\n location: string\n /**\n * Price details.\n */\n price: PriceDetails\n /**\n * Images of the listing with optional dimensions.\n */\n images: Array<{\n url: string\n dimensions?: string\n }>\n /**\n * Tags/Hashtags (i.e. categories, keywords, etc.)\n */\n hashtags: string[]\n /**\n * Other standard tags.\n * @example \"g\", a geohash for more precise location\n */\n additionalTags: Record<string, string | string[]>\n}\n\n/**\n * Validates an event to ensure it is a valid classified listing event.\n * @param event - The event to validate.\n * @returns True if the event is valid, false otherwise.\n */\nexport function validateEvent(event: Event): boolean {\n if (![ClassifiedListing, DraftClassifiedListing].includes(event.kind)) return false\n\n const requiredTags = ['d', 'title', 'summary', 'location', 'published_at', 'price']\n const requiredTagCount = requiredTags.length\n const tagCounts: Record<string, number> = {}\n\n if (event.tags.length < requiredTagCount) return false\n\n for (const tag of event.tags) {\n if (tag.length < 2) return false\n\n const [tagName, ...tagValues] = tag\n\n if (tagName == 'published_at') {\n const timestamp = parseInt(tagValues[0])\n if (isNaN(timestamp)) return false\n } else if (tagName == 'price') {\n if (tagValues.length < 2) return false\n\n const price = parseInt(tagValues[0])\n if (isNaN(price) || tagValues[1].length != 3) return false\n } else if ((tagName == 'e' || tagName == 'a') && tag.length != 3) {\n return false\n }\n\n if (requiredTags.includes(tagName)) {\n tagCounts[tagName] = (tagCounts[tagName] || 0) + 1\n }\n }\n\n return Object.values(tagCounts).every(count => count == 1) && Object.keys(tagCounts).length == requiredTagCount\n}\n\n/**\n * Parses an event and returns a classified listing object.\n * @param event - The event to parse.\n * @returns The classified listing object.\n * @throws Error if the event is invalid.\n */\nexport function parseEvent(event: Event): ClassifiedListingObject {\n if (!validateEvent(event)) {\n throw new Error('Invalid event')\n }\n\n const listing: ClassifiedListingObject = {\n isDraft: event.kind === DraftClassifiedListing,\n title: '',\n summary: '',\n content: event.content,\n publishedAt: '',\n location: '',\n price: {\n amount: '',\n currency: '',\n },\n images: [],\n hashtags: [],\n additionalTags: {},\n }\n\n for (let i = 0; i < event.tags.length; i++) {\n const tag = event.tags[i]\n const [tagName, ...tagValues] = tag\n\n if (tagName == 'title') {\n listing.title = tagValues[0]\n } else if (tagName == 'summary') {\n listing.summary = tagValues[0]\n } else if (tagName == 'published_at') {\n listing.publishedAt = tagValues[0]\n } else if (tagName == 'location') {\n listing.location = tagValues[0]\n } else if (tagName == 'price') {\n listing.price.amount = tagValues[0]\n listing.price.currency = tagValues[1]\n\n if (tagValues.length == 3) {\n listing.price.frequency = tagValues[2]\n }\n } else if (tagName == 'image') {\n listing.images.push({\n url: tagValues[0],\n dimensions: tagValues?.[1] ?? undefined,\n })\n } else if (tagName == 't') {\n listing.hashtags.push(tagValues[0])\n } else if (tagName == 'e' || tagName == 'a') {\n listing.additionalTags[tagName] = [...tagValues]\n }\n }\n\n return listing\n}\n\n/**\n * Generates an event template based on a classified listing object.\n *\n * @param listing - The classified listing object.\n * @returns The event template.\n */\nexport function generateEventTemplate(listing: ClassifiedListingObject): EventTemplate {\n const priceTag = ['price', listing.price.amount, listing.price.currency]\n if (listing.price.frequency) priceTag.push(listing.price.frequency)\n\n const tags: string[][] = [\n ['d', listing.title.trim().toLowerCase().replace(/ /g, '-')],\n ['title', listing.title],\n ['published_at', listing.publishedAt],\n ['summary', listing.summary],\n ['location', listing.location],\n priceTag,\n ]\n\n for (let i = 0; i < listing.images.length; i++) {\n const image = listing.images[i]\n const imageTag = ['image', image.url]\n if (image.dimensions) imageTag.push(image.dimensions)\n\n tags.push(imageTag)\n }\n\n for (let i = 0; i < listing.hashtags.length; i++) {\n const t = listing.hashtags[i]\n\n tags.push(['t', t])\n }\n\n for (const [key, value] of Object.entries(listing.additionalTags)) {\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const val = value[i]\n\n tags.push([key, val])\n }\n } else {\n tags.push([key, value])\n }\n }\n\n return {\n kind: listing.isDraft ? DraftClassifiedListing : ClassifiedListing,\n content: listing.content,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n }\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number) {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number) {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number) {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number) {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
4
+ "sourcesContent": ["import { Event, EventTemplate } from './core.ts'\nimport { ClassifiedListing, DraftClassifiedListing } from './kinds.ts'\n\n/**\n * Represents the details of a price.\n * @example { amount: '100', currency: 'USD', frequency: 'month' }\n * @example { amount: '100', currency: 'EUR' }\n */\nexport type PriceDetails = {\n /**\n * The amount of the price.\n */\n amount: string\n /**\n * The currency of the price in 3-letter ISO 4217 format.\n * @example 'USD'\n */\n currency: string\n /**\n * The optional frequency of payment.\n * Can be one of: 'hour', 'day', 'week', 'month', 'year', or a custom string.\n */\n frequency?: string\n}\n\n/**\n * Represents a classified listing object.\n */\nexport type ClassifiedListingObject = {\n /**\n * Whether the listing is a draft or not.\n */\n isDraft: boolean\n /**\n * A title of the listing.\n */\n title: string\n /**\n * A short summary or tagline.\n */\n summary: string\n /**\n * A description in Markdown format.\n */\n content: string\n /**\n * Timestamp in unix seconds of when the listing was published.\n */\n publishedAt: string\n /**\n * Location of the listing.\n * @example 'NYC'\n */\n location: string\n /**\n * Price details.\n */\n price: PriceDetails\n /**\n * Images of the listing with optional dimensions.\n */\n images: Array<{\n url: string\n dimensions?: string\n }>\n /**\n * Tags/Hashtags (i.e. categories, keywords, etc.)\n */\n hashtags: string[]\n /**\n * Other standard tags.\n * @example \"g\", a geohash for more precise location\n */\n additionalTags: Record<string, string | string[]>\n}\n\n/**\n * Validates an event to ensure it is a valid classified listing event.\n * @param event - The event to validate.\n * @returns True if the event is valid, false otherwise.\n */\nexport function validateEvent(event: Event): boolean {\n if (![ClassifiedListing, DraftClassifiedListing].includes(event.kind)) return false\n\n const requiredTags = ['d', 'title', 'summary', 'location', 'published_at', 'price']\n const requiredTagCount = requiredTags.length\n const tagCounts: Record<string, number> = {}\n\n if (event.tags.length < requiredTagCount) return false\n\n for (const tag of event.tags) {\n if (tag.length < 2) return false\n\n const [tagName, ...tagValues] = tag\n\n if (tagName == 'published_at') {\n const timestamp = parseInt(tagValues[0])\n if (isNaN(timestamp)) return false\n } else if (tagName == 'price') {\n if (tagValues.length < 2) return false\n\n const price = parseInt(tagValues[0])\n if (isNaN(price) || tagValues[1].length != 3) return false\n } else if ((tagName == 'e' || tagName == 'a') && tag.length != 3) {\n return false\n }\n\n if (requiredTags.includes(tagName)) {\n tagCounts[tagName] = (tagCounts[tagName] || 0) + 1\n }\n }\n\n return Object.values(tagCounts).every(count => count == 1) && Object.keys(tagCounts).length == requiredTagCount\n}\n\n/**\n * Parses an event and returns a classified listing object.\n * @param event - The event to parse.\n * @returns The classified listing object.\n * @throws Error if the event is invalid.\n */\nexport function parseEvent(event: Event): ClassifiedListingObject {\n if (!validateEvent(event)) {\n throw new Error('Invalid event')\n }\n\n const listing: ClassifiedListingObject = {\n isDraft: event.kind === DraftClassifiedListing,\n title: '',\n summary: '',\n content: event.content,\n publishedAt: '',\n location: '',\n price: {\n amount: '',\n currency: '',\n },\n images: [],\n hashtags: [],\n additionalTags: {},\n }\n\n for (let i = 0; i < event.tags.length; i++) {\n const tag = event.tags[i]\n const [tagName, ...tagValues] = tag\n\n if (tagName == 'title') {\n listing.title = tagValues[0]\n } else if (tagName == 'summary') {\n listing.summary = tagValues[0]\n } else if (tagName == 'published_at') {\n listing.publishedAt = tagValues[0]\n } else if (tagName == 'location') {\n listing.location = tagValues[0]\n } else if (tagName == 'price') {\n listing.price.amount = tagValues[0]\n listing.price.currency = tagValues[1]\n\n if (tagValues.length == 3) {\n listing.price.frequency = tagValues[2]\n }\n } else if (tagName == 'image') {\n listing.images.push({\n url: tagValues[0],\n dimensions: tagValues?.[1] ?? undefined,\n })\n } else if (tagName == 't') {\n listing.hashtags.push(tagValues[0])\n } else if (tagName == 'e' || tagName == 'a') {\n listing.additionalTags[tagName] = [...tagValues]\n }\n }\n\n return listing\n}\n\n/**\n * Generates an event template based on a classified listing object.\n *\n * @param listing - The classified listing object.\n * @returns The event template.\n */\nexport function generateEventTemplate(listing: ClassifiedListingObject): EventTemplate {\n const priceTag = ['price', listing.price.amount, listing.price.currency]\n if (listing.price.frequency) priceTag.push(listing.price.frequency)\n\n const tags: string[][] = [\n ['d', listing.title.trim().toLowerCase().replace(/ /g, '-')],\n ['title', listing.title],\n ['published_at', listing.publishedAt],\n ['summary', listing.summary],\n ['location', listing.location],\n priceTag,\n ]\n\n for (let i = 0; i < listing.images.length; i++) {\n const image = listing.images[i]\n const imageTag = ['image', image.url]\n if (image.dimensions) imageTag.push(image.dimensions)\n\n tags.push(imageTag)\n }\n\n for (let i = 0; i < listing.hashtags.length; i++) {\n const t = listing.hashtags[i]\n\n tags.push(['t', t])\n }\n\n for (const [key, value] of Object.entries(listing.additionalTags)) {\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const val = value[i]\n\n tags.push([key, val])\n }\n } else {\n tags.push([key, value])\n }\n }\n\n return {\n kind: listing.isDraft ? DraftClassifiedListing : ClassifiedListing,\n content: listing.content,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n }\n}\n", "/** Events are **regular**, which means they're all expected to be stored by relays. */\nexport function isRegularKind(kind: number): boolean {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nexport function isReplaceableKind(kind: number): boolean {\n return [0, 3].includes(kind) || (10000 <= kind && kind < 20000)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nexport function isEphemeralKind(kind: number): boolean {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nexport function isParameterizedReplaceableKind(kind: number): boolean {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\nexport type KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nexport function classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport const Metadata = 0\nexport const ShortTextNote = 1\nexport const RecommendRelay = 2\nexport const Contacts = 3\nexport const EncryptedDirectMessage = 4\nexport const EncryptedDirectMessages = 4\nexport const EventDeletion = 5\nexport const Repost = 6\nexport const Reaction = 7\nexport const BadgeAward = 8\nexport const GenericRepost = 16\nexport const ChannelCreation = 40\nexport const ChannelMetadata = 41\nexport const ChannelMessage = 42\nexport const ChannelHideMessage = 43\nexport const ChannelMuteUser = 44\nexport const OpenTimestamps = 1040\nexport const FileMetadata = 1063\nexport const LiveChatMessage = 1311\nexport const ProblemTracker = 1971\nexport const Report = 1984\nexport const Reporting = 1984\nexport const Label = 1985\nexport const CommunityPostApproval = 4550\nexport const JobRequest = 5999\nexport const JobResult = 6999\nexport const JobFeedback = 7000\nexport const ZapGoal = 9041\nexport const ZapRequest = 9734\nexport const Zap = 9735\nexport const Highlights = 9802\nexport const Mutelist = 10000\nexport const Pinlist = 10001\nexport const RelayList = 10002\nexport const BookmarkList = 10003\nexport const CommunitiesList = 10004\nexport const PublicChatsList = 10005\nexport const BlockedRelaysList = 10006\nexport const SearchRelaysList = 10007\nexport const InterestsList = 10015\nexport const UserEmojiList = 10030\nexport const FileServerPreference = 10096\nexport const NWCWalletInfo = 13194\nexport const LightningPubRPC = 21000\nexport const ClientAuth = 22242\nexport const NWCWalletRequest = 23194\nexport const NWCWalletResponse = 23195\nexport const NostrConnect = 24133\nexport const HTTPAuth = 27235\nexport const Followsets = 30000\nexport const Genericlists = 30001\nexport const Relaysets = 30002\nexport const Bookmarksets = 30003\nexport const Curationsets = 30004\nexport const ProfileBadges = 30008\nexport const BadgeDefinition = 30009\nexport const Interestsets = 30015\nexport const CreateOrUpdateStall = 30017\nexport const CreateOrUpdateProduct = 30018\nexport const LongFormArticle = 30023\nexport const DraftLong = 30024\nexport const Emojisets = 30030\nexport const Application = 30078\nexport const LiveEvent = 30311\nexport const UserStatuses = 30315\nexport const ClassifiedListing = 30402\nexport const DraftClassifiedListing = 30403\nexport const Date = 31922\nexport const Time = 31923\nexport const Calendar = 31924\nexport const CalendarEventRSVP = 31925\nexport const Handlerrecommendation = 31989\nexport const Handlerinformation = 31990\nexport const CommunityDefinition = 34550\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
package/lib/cjs/pool.js CHANGED
@@ -340,16 +340,19 @@ var AbstractRelay = class {
340
340
  this.ws.onerror = (ev) => {
341
341
  reject(ev.message);
342
342
  if (this._connected) {
343
+ this._connected = false;
344
+ this.connectionPromise = void 0;
343
345
  this.onclose?.();
344
346
  this.closeAllSubscriptions("relay connection errored");
345
- this._connected = false;
346
347
  }
347
348
  };
348
349
  this.ws.onclose = async () => {
349
- this.connectionPromise = void 0;
350
- this.onclose?.();
351
- this.closeAllSubscriptions("relay connection closed");
352
- this._connected = false;
350
+ if (this._connected) {
351
+ this._connected = false;
352
+ this.connectionPromise = void 0;
353
+ this.onclose?.();
354
+ this.closeAllSubscriptions("relay connection closed");
355
+ }
353
356
  };
354
357
  this.ws.onmessage = this._onmessage.bind(this);
355
358
  });
@@ -542,7 +545,7 @@ var Subscription = class {
542
545
  this.oneose?.();
543
546
  }
544
547
  close(reason = "closed by caller") {
545
- if (!this.closed) {
548
+ if (!this.closed && this.relay.connected) {
546
549
  this.relay.send('["CLOSE",' + JSON.stringify(this.id) + "]");
547
550
  this.closed = true;
548
551
  }