{"id":12478,"date":"2026-10-19T08:26:22","date_gmt":"2026-10-18T23:26:22","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=12478"},"modified":"2026-10-21T10:36:13","modified_gmt":"2026-10-21T01:36:13","slug":"nextjs-sns-server-51-6-2","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/10\/19\/nextjs-sns-server-51-6-2\/","title":{"rendered":"[nextjs]SNS Server-51(myapp61+)[signedFetch,getActorData]\u00a0"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb \uc774\uc804 \ud3ec\uc2a4\ud2b8\uc5d0\uc11c \uc88b\uc544\uc694,\ubd80\uc2a4\ud2b8 \uae30\ub2a5\uc5d0 \ub300\ud574\uc11c \uc0b4\ud3b4\ubd24\uc2b5\ub2c8\ub2e4.<br>In the previous post, we looked at the &#8220;like&#8221; and &#8220;boost&#8221; features.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc774\ubc88\uc5d0\ub294 \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0 \uc88b\uc544\uc694,\ubd80\uc2a4\ud2b8\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uae30 \uc704\ud574\uc11c \uc0ac\uc6a9\ud558\ub294 \ud568\uc218\uc5d0 \ub300\ud574\uc11c \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.<br>Next, we will take a look at the function used to update &#8220;likes&#8221; and &#8220;boosts&#8221; on a remote server.<\/p>\n\n\n\n<p>\ud83d\udcc1 lib\/ap.ts<\/p>\n\n\n\n<p>\u2714\ufe0f signedFetch \ud568\uc218 \/ function<\/p>\n\n\n\n<p>&#8212; \ub9ac\ubaa8\ud2b8\uc11c\ubc84\uc5d0 \ub0b4 private key\ub85c \uc11c\uba85\ud574\uc11c \uc804\uc1a1\ud558\uace0 \uc751\ub2f5\uacb0\uacfc\ub97c \ubc1b\uc2b5\ub2c8\ub2e4.<br>I sign the request with my private key, send it to the remote server, and receive the response.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>... ...\nconst DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';\nconst KEY_PATH = path.join(process.cwd(), 'data\/keys\/private.pem');\nconst PRIVATE_KEY = fs.readFileSync(KEY_PATH, 'utf8');\n... ...\nexport async function signedFetch(urlStr: string, username: string) {\n  const actorId = `https:\/\/${DOMAIN}\/users\/${username}`;\n  const privateKey = getPrivateKey(username);\n  const url = new URL(urlStr);\n  const date = new Date().toUTCString();\n  const signingString = `(request-target): get ${url.pathname}${url.search}\\nhost: ${url.host}\\ndate: ${date}`;\n  \n  const signer = crypto.createSign('sha256');\n  signer.update(signingString);\n  const signature = signer.sign(privateKey, 'base64');\n\n  const keyId = `${actorId}#main-key`;\n  const sigHeader = `keyId=\"${keyId}\",headers=\"(request-target) host date\",signature=\"${signature}\"`;\n\n  console.log(`\ud83d\udd10 Signed GET -&gt; ${urlStr} as ${username}`);\n\n  return fetch(urlStr, {\n    headers: {\n      'Accept': 'application\/activity+json',\n      'Date': date,\n      'Signature': sigHeader,\n      'Host': url.host\n    }\n  });\n}<\/code><\/pre>\n\n\n\n<p>1)getPrivateKey()\ud568\uc218 \/ function<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>  const privateKey = getPrivateKey(username);<\/code><\/pre>\n\n\n\n<p>A.signedFetch\ud568\uc218\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 getPrivateKey\ud568\uc218 \uc785\ub2c8\ub2e4.<br>This is the <code>getPrivateKey<\/code> function used by the <code>signedFetch<\/code> function.<\/p>\n\n\n\n<p>B.\ub85c\uadf8\uc778\ud55c \uc544\uc774\ub514\uac00 user1\uc774\uba74 \ub808\uac70\uc2dc \uacbd\ub85c\uc5d0\uc11c \ud0a4\ud30c\uc77c\uc744 \uac00\uc838\uc635\ub2c8\ub2e4.<br>If the logged-in ID is user1, retrieve the key file from the legacy path.<br>(data\/keys\/private.pem&#8217;)<\/p>\n\n\n\n<p>C.\ub2e4\ub978 \uc0ac\uc6a9\uc790\uc778 \uacbd\uc6b0 data\/keys\/${username}\/private.pem \uc774 \uacbd\ub85c\uc5d0\uc11c \ud0a4\ud30c\uc77c\uc744 \uac00\uc838\uc635\ub2c8\ub2e4.<br>For other users, the key file is retrieved from the path: data\/keys\/${username}\/private.pem.<\/p>\n\n\n\n<p>D.\ud0a4 \ud30c\uc77c\uc740 \ud68c\uc6d0\uac00\uc785\uc2dc \uc790\ub3d9\uc0dd\uc131\ub429\ub2c8\ub2e4.<br>A key file is automatically generated upon registration.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>function getPrivateKey(username: string): string {\n  \/\/ data\/keys\/user1\/private.pem \ud615\ud0dc \uc9c0\uc6d0\n  const userKeyPath = path.join(process.cwd(), `data\/keys\/${username}\/private.pem`);\n  const legacyKeyPath = path.join(process.cwd(), 'data\/keys\/private.pem');\n  \n  try {\n    if (fs.existsSync(userKeyPath)) {\n      return fs.readFileSync(userKeyPath, 'utf8');\n    }\n    \/\/ user1\uc774\uba74 legacy \uacbd\ub85c\ub3c4 \ud655\uc778 \/ Check legacy path if username is user1\n    if (fs.existsSync(legacyKeyPath)) {\n      return fs.readFileSync(legacyKeyPath, 'utf8');\n    }\n    \/\/ data\/keys\/ \ub514\ub809\ud1a0\ub9ac\uc5d0\uc11c \uc9c1\uc811 \/ Check directly in data\/keys\/ directory\n    const directPath = path.join(process.cwd(), `data\/keys\/${username}.pem`);\n    if (fs.existsSync(directPath)) {\n      return fs.readFileSync(directPath, 'utf8');\n    }\n  } catch (e) {\n    console.error(`\ud83d\udd11 \ud0a4 \ub85c\ub4dc \uc2e4\ud328 \/ Key load failed for ${username}:`, e);\n  }\n  throw new Error(`Private key not found for ${username}`);\n}<\/code><\/pre>\n\n\n\n<p>2) private \ud0a4 \uc554\ud638\ud654 \/ private key encryption<\/p>\n\n\n\n<p>A. \uc138\uc0c1\uc5d0\uc11c \uc720\uc77c\ud55c \ubb38\uc790\uc5f4\uc744 sha256\uc774\ub77c\ub294 \ubc29\uc2dd\uc73c\ub85c \ub9cc\ub4e4\uae30<br>Creating a unique string using the SHA-256 method<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>const signer = crypto.createSign('sha256');\nsigner.update(signingString);<\/code><\/pre>\n\n\n\n<p>B.sha256\uc73c\ub85c \ub9cc\ub4e4\uba74 16\uc9c4\uc218 \ubc14\uc774\ub108\ub9ac \ud615\ud0dc\uac00 \ub428 \uc774\uac78 RSA\ub85c \uc554\ud638\ud654\ud558\uace0 \ub2e4\uc2dc \ubb38\uc790\uc5f4\ub85c \ubcc0\ud658<br>Generating a hash using SHA-256 results in a hexadecimal binary format; this is then encrypted via RSA and converted back into a string.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code> const signature = signer.sign(privateKey, 'base64');<\/code><\/pre>\n\n\n\n<p>3) \uc870\ub9bd\ub41c \uc815\ubcf4\ub97c \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0 \uc804\uc1a1\ud558\uace0 \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0\uc11c \ubc1b\uc740 \uc751\ub2f5 \uacb0\uacfc\ub97c \ub9ac\ud134<br>Transmit the assembled information to a remote server and return the response received from that server.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>  return fetch(urlStr, {\n    headers: {\n      'Accept': 'application\/activity+json',\n      'Date': date,\n      'Signature': sigHeader,\n      'Host': url.host\n    }\n  });<\/code><\/pre>\n\n\n\n<p>A.\uc751\ub2f5\uacb0\uacfc\ub97c \uc88b\uc544\uc694\ub098 \ubd80\uc2a4\ud2b8 \ucf54\ub4dc\uc5d0\uc11c \uc544\ub798\uc640 \uac19\uc774 \uc0ac\uc6a9\ub428.<br>The response result is used as shown below in the code for &#8220;likes&#8221; or &#8220;boosts.&#8221;<\/p>\n\n\n\n<p>a)private\ud0a4\ub85c \uc11c\uba85\ud574\uc11c \ub9ac\ubaa8\ud2b8\uc11c\ubc84\uc5d0 \uc804\uc1a1\ud558\uba74 \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0\uc11c \ub0b4 public key\ub97c \ud638\ucd9c\ud568.<br>When you sign with your private key and send it to a remote server, the remote server retrieves your public key.<\/p>\n\n\n\n<p>b)\ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0\uc11c \ub0b4 private\ud0a4\uc640 public\ud0a4\uac00 \uc77c\uce58\ud558\uba74 pr.ok = true\ub97c \ub9ac\ud134\ud574 \uc90c<br>If my private key and public key match on the remote server, it returns <code>pr.ok = true<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code> const pr = await signedFetch(target, username);\n if (pr.ok) { ... }<\/code><\/pre>\n\n\n\n<p>B.\ubc18\ub300\ub85c \ub0b4 \uc11c\ubc84\uc5d0\uc11c \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0 public key\uc694\uccad\ud558\ub294 \ubd80\ubd84 \ucf54\ub4dc\ub294 \uc544\ub798\uc640 \uac19\uc74c<br>Conversely, the code on my server that requests a public key from the remote server is as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code># app\/users\/&#91;username]\/inbox\/route.ts\n... ...  \n   const { isValid } = await fetchActorPublicKeyWithRetry(\n      actorIdForVerify,\n      username,\n      verifyReq\n   );\n... ...<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\u2714\ufe0f getActorData \ud568\uc218 \/ function<\/p>\n\n\n\n<p>&#8212; signedFetch\uc758 \uc751\ub2f5 \ub370\uc774\ud130\ub97c \uadf8\ub300\ub85c \ud65c\uc6a9\ud558\ub294 \ud568\uc218<br>A function that utilizes the response data from signedFetch as-is<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>export async function getActorData(actorUrl: string, username: string) {\n  const res = await signedFetch(actorUrl, username);\n  if (!res.ok) throw new Error(`getActorData failed: ${res.status} ${await res.text()}`);\n  const data = await res.json();\n  return { id: data.id, inbox: data.inbox, data };\n}<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\ub9c8\ud0dc\ubcf5\uc74c 21\uc7a5 22\uc808 \/ Matthew 21: 22<\/p>\n\n\n\n<p>&#8220;\ub108\ud76c\uac00 \uae30\ub3c4\ud560 \ub54c\uc5d0 \ubb34\uc5c7\uc774\ub4e0\uc9c0 \ubbff\uace0 \uad6c\ud558\ub294 \uac83\uc740 \ub2e4 \ubc1b\uc73c\ub9ac\ub77c \ud558\uc2dc\ub2c8\ub77c&#8221;<\/p>\n\n\n\n<p>&#8220;And everything you ask in prayer, believing, you will receive.&#8221;<\/p>\n<\/blockquote>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb \uc774\uc804 \ud3ec\uc2a4\ud2b8\uc5d0\uc11c \uc88b\uc544\uc694,\ubd80\uc2a4\ud2b8 \uae30\ub2a5\uc5d0 \ub300\ud574\uc11c \uc0b4\ud3b4\ubd24\uc2b5\ub2c8\ub2e4.In the previous post, we looked at the &#8220;like&#8221; and &#8220;boost&#8221; features. \ud83d\udc49\ud83c\udffb \uc774\ubc88\uc5d0\ub294 \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc5d0 \uc88b\uc544\uc694,\ubd80\uc2a4\ud2b8\ub97c \uc5c5\ub370\uc774\ud2b8\ud558\uae30 \uc704\ud574\uc11c \uc0ac\uc6a9\ud558\ub294 \ud568\uc218\uc5d0 \ub300\ud574\uc11c \uc0b4\ud3b4\ubcf4\uaca0\uc2b5\ub2c8\ub2e4.Next, we will take a look at the function used to update &#8220;likes&#8221; and &#8220;boosts&#8221; on a remote server. \ud83d\udcc1 lib\/ap.ts \u2714\ufe0f signedFetch \ud568\uc218 \/ function &#8212; [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[28,27,1,7],"tags":[],"class_list":["post-12478","post","type-post","status-publish","format-standard","hentry","category-news","category-nextjs","category-uncategorized","category-website","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/12478","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=12478"}],"version-history":[{"count":29,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/12478\/revisions"}],"predecessor-version":[{"id":12510,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/12478\/revisions\/12510"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=12478"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=12478"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=12478"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}