Appearance
Per-page sharing
How a target app derives the per-page token from the share seed and serves the shared page — with stdlib-only examples in Go, Ruby, JavaScript, and Python.
You get the three inputs from the page-share reveal modal — in the cloud console's sessions page or the gateway's local sessions page.
How it works
The gateway mints a page-share key for your app: a base token of shape nh.sid.ts.mac (opaque — never parse it beyond splitting at the last dot) and a 32-byte share seed (base64url, revealed once). The reveal modal shows three values: the public host, the base token, and the seed. From these your app derives one short token per page and builds a public link to exactly that page — no other page is reachable through it.
The security model: the gateway is the sole verifier — your app only derives tokens, it never validates them. The seed stays server-side in your app and is never sent to a browser. Revoking the base session at the gateway kills every per-page link minted from it at once.
Derivation
| Page token (22 chars) | b64url_nopad( HMAC-SHA256(key=seed, msg="page\n" + host + "\n" + path)[:16] ) |
| Share URL | https://<host><path>?<mac>=<nh.sid.ts>&<page_token>=p |
pathexcludes any query string.- The base token splits at its last dot: the trailing
macbecomes the first query param's key, the rest (nh.sid.ts) its value. A base token without a dot is invalid — reject it. - The derived page token is the second param's key, with the literal value
p. - The seed is base64url (accept padded or unpadded) and must decode to exactly 32 bytes.
Examples
Stdlib-only, no dependencies. Each snippet prints the share URL for the test vector below.
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
)
// pageShareToken derives the 22-char per-page token:
// b64url_nopad( HMAC-SHA256(seed, "page\n"+host+"\n"+path)[:16] )
func pageShareToken(seed []byte, host, path string) string {
mac := hmac.New(sha256.New, seed)
mac.Write([]byte("page\n" + host + "\n" + path))
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)[:16])
}
// pageShareURL assembles https://<host><path>?<mac>=<nh.sid.ts>&<token>=p
func pageShareURL(seedB64, host, baseToken, path string) (string, error) {
seed, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(seedB64, "="))
if err != nil {
return "", fmt.Errorf("seed is not base64url: %w", err)
}
if len(seed) != 32 {
return "", fmt.Errorf("seed decodes to %d bytes, want 32", len(seed))
}
i := strings.LastIndexByte(baseToken, '.')
if i <= 0 || i == len(baseToken)-1 {
return "", errors.New("base token has no value.mac split")
}
value, key := baseToken[:i], baseToken[i+1:]
return "https://" + host + path +
"?" + key + "=" + value +
"&" + pageShareToken(seed, host, path) + "=p", nil
}
func main() {
url, err := pageShareURL(
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
"example.com", "nh.sid.ts.mac", "/docs/example")
if err != nil {
panic(err)
}
fmt.Println(url)
}ruby
require "openssl"
require "base64"
# b64url_nopad( HMAC-SHA256(seed, "page\n" + host + "\n" + path)[0,16] )
def page_share_token(seed, host, path)
mac = OpenSSL::HMAC.digest("SHA256", seed, "page\n#{host}\n#{path}")
Base64.urlsafe_encode64(mac[0, 16], padding: false)
end
# https://<host><path>?<mac>=<nh.sid.ts>&<token>=p
def page_share_url(seed_b64, host, base_token, path)
s = seed_b64.delete("=")
seed = Base64.urlsafe_decode64(s + "=" * (-s.length % 4))
raise "seed decodes to #{seed.bytesize} bytes, want 32" unless seed.bytesize == 32
i = base_token.rindex(".")
raise "base token has no value.mac split" if i.nil? || i.zero? || i == base_token.length - 1
value, key = base_token[0...i], base_token[i + 1..]
"https://#{host}#{path}?#{key}=#{value}&#{page_share_token(seed, host, path)}=p"
end
puts page_share_url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
"example.com", "nh.sid.ts.mac", "/docs/example")js
const crypto = require("crypto");
// b64url_nopad( HMAC-SHA256(seed, "page\n" + host + "\n" + path).slice(0, 16) )
function pageShareToken(seed, host, path) {
const mac = crypto.createHmac("sha256", seed)
.update(`page\n${host}\n${path}`).digest();
return mac.subarray(0, 16).toString("base64url");
}
// https://<host><path>?<mac>=<nh.sid.ts>&<token>=p
function pageShareURL(seedB64, host, baseToken, path) {
const seed = Buffer.from(seedB64.replace(/=+$/, ""), "base64url");
if (seed.length !== 32) throw new Error(`seed decodes to ${seed.length} bytes, want 32`);
const i = baseToken.lastIndexOf(".");
if (i <= 0 || i === baseToken.length - 1) throw new Error("base token has no value.mac split");
const value = baseToken.slice(0, i), key = baseToken.slice(i + 1);
return `https://${host}${path}?${key}=${value}&${pageShareToken(seed, host, path)}=p`;
}
console.log(pageShareURL("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
"example.com", "nh.sid.ts.mac", "/docs/example"));python
import base64
import hashlib
import hmac
def page_share_token(seed: bytes, host: str, path: str) -> str:
"""b64url_nopad( HMAC-SHA256(seed, "page\\n" + host + "\\n" + path)[:16] )"""
mac = hmac.new(seed, f"page\n{host}\n{path}".encode(), hashlib.sha256).digest()
return base64.urlsafe_b64encode(mac[:16]).rstrip(b"=").decode()
def page_share_url(seed_b64: str, host: str, base_token: str, path: str) -> str:
"""https://<host><path>?<mac>=<nh.sid.ts>&<token>=p"""
s = seed_b64.rstrip("=")
seed = base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
if len(seed) != 32:
raise ValueError(f"seed decodes to {len(seed)} bytes, want 32")
i = base_token.rfind(".")
if i <= 0 or i == len(base_token) - 1:
raise ValueError("base token has no value.mac split")
value, key = base_token[:i], base_token[i + 1:]
return f"https://{host}{path}?{key}={value}&{page_share_token(seed, host, path)}=p"
print(page_share_url("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
"example.com", "nh.sid.ts.mac", "/docs/example"))Test vector
Verify before going live
These inputs must produce exactly this page token:
| seed | AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8 |
| host | example.com |
| path | /docs/example |
| token | 3jgF8OH9AxuQHTtySu-3BQ |