2026-09-09, 05:07
  #97
Medlem
The Crashs avatar
Tycker detta beskriver ganska bra vad jag försöker göra nu. Det är alltså ingen skillnad mot att besöka min länk och flashback i principen.
Från OP-anonymous-identity.md
Kod:
---
title: Anonymous identity
status: prototype
major: v1
fork: F0
---

# Anonymous identity

## Why — 30 seconds

The prototype needs enough continuity to make trivial vote abuse inconvenient without
pretending it knows who a person is.

## How

```text
browser metadata
      ↓
local heuristic identity
      ↓
@anotheruser########
```

This identifies a browser-like client among visitors.

It does not claim to identify a person or Flashback account.

## Accepted error

Two different browsers may occasionally collapse into one local identity.

For this prototype that is accepted technical risk because the consequence is limited to
low-risk anti-abuse behavior.

## Hard boundary

IP address is not an identity signal.

## Session behavior

Anonymous continuity is the default.

No conventional logout flow is required.

The identity exists to support product behavior, not to create an account-management
system.

Eller mer tekniskt


Önskar jag kunde skriva källkoden här, men den senaste är på 19 000 tecken (ca 400 rader), och den återknyter till hur vi borde bedöma istället för en gillaknapp.

Men här är vår identifierare på Flashback.
Transparens i sin elegans.
Kod:
// FlashbackNormalizer — the single boundary that turns Flashback's (varying) upstream encoding
// into canonical UTF-8. Everything downstream (parsers, API, storage) assumes UTF-8 and must never
// know or care which encoding Flashback originally used. Do NOT decode bytes anywhere else.
//
// Flashback can differ in encoding between responses/endpoints, so encoding is detected PER RESPONSE:
//   1. HTTP Content-Type charset
//   2. BOM
//   3. <meta charset> / <meta http-equiv content-type>
//   4. fallback heuristic (Flashback's legacy default: ISO-8859-1)

// Honest, identifying User-Agent so Flashback can attribute our traffic and reach the operator
// directly (native PM link) if anything pushes too hard — instead of blanket-blocking an anonymous
// scraper. Every FlashbackNormalizer fetch sends this.
const UA =
  'Flashbackaren/0.1 (prototyp; kontakta oss via Flashback-PM: https://www.flashback.org/private.php?do=newpm&u=135422)';

export interface FlashbackResponse {
  ok: boolean;
  status: number;
  finalUrl: string; // response URL after redirects (or the requested URL)
  location: string | null; // redirect target, when fetched with redirect:'manual'
  html: string; // canonical UTF-8
  encoding: string; // the upstream encoding we detected (source metadata, for debugging)
  sourceUrl: string; // canonical source reference (the URL we requested)
}

function isValidUtf8(bytes: Uint8Array): boolean {
  try {
    new TextDecoder('utf-8', { fatal: true }).decode(bytes);
    return true;
  } catch {
    return false;
  }
}

export function detectEncoding(contentType: string | null, bytes: Uint8Array): string {
  // 1. HTTP Content-Type charset (authoritative)
  const ct = /charset=["']?\s*([\w-]+)/i.exec(contentType ?? '');
  if (ct) return ct[1].toLowerCase();
  // 2. BOM
  if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) return 'utf-8';
  if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) return 'utf-16le';
  if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) return 'utf-16be';
  // 3. UTF-8 validity sniff — trust real UTF-8 bytes over a possibly-stale <meta> declaration.
  //    Browser-saved Flashback pages are UTF-8 even though the embedded <meta> still says
  //    ISO-8859-1; live latin1 pages contain isolated high bytes that are invalid UTF-8, so they
  //    correctly fall through to the meta/legacy branch below.
  if (isValidUtf8(bytes)) return 'utf-8';
  // 4. <meta charset> / <meta http-equiv=content-type ... charset=...> (scan the ASCII-safe head)
  const head = new TextDecoder('iso-8859-1').decode(bytes.subarray(0, 4096));
  const meta =
    /<meta[^>]+charset=["']?\s*([\w-]+)/i.exec(head) ??
    /<meta[^>]+http-equiv=["']?content-type["'][^>]*charset=["']?\s*([\w-]+)/i.exec(head);
  if (meta) return meta[1].toLowerCase();
  // 5. fallback: Flashback's legacy default
  return 'iso-8859-1';
}

function decodeToUtf8(bytes: Uint8Array, encoding: string): string {
  try {
    return new TextDecoder(encoding).decode(bytes);
  } catch {
    return new TextDecoder('iso-8859-1').decode(bytes); // last-resort; never throw out of the boundary
  }
}

// Normalize already-obtained bytes (e.g. an operator-uploaded saved page) to a canonical response.
export function normalizeBytes(bytes: Uint8Array, sourceUrl: string, contentType: string | null = null): FlashbackResponse {
  const encoding = detectEncoding(contentType, bytes);
  return { ok: true, status: 200, finalUrl: sourceUrl, location: null, html: decodeToUtf8(bytes, encoding), encoding, sourceUrl };
}

// Fetch a Flashback URL and return canonical UTF-8 + source metadata. This is the ONLY place
// Flashback bytes are decoded.
export async function fetchFlashback(
  url: string,
  opts: { redirect?: 'manual' | 'follow' } = {},
): Promise<FlashbackResponse> {
  const res = await fetch(url, { headers: { 'User-Agent': UA }, redirect: opts.redirect ?? 'follow' });
  const bytes = new Uint8Array(await res.arrayBuffer());
  const encoding = detectEncoding(res.headers.get('Content-Type'), bytes);
  return {
    ok: res.ok,
    status: res.status,
    finalUrl: res.url || url,
    location: res.headers.get('Location'),
    html: decodeToUtf8(bytes, encoding),
    encoding,
    sourceUrl: url,
  };
}
__________________
Senast redigerad av The Crash 2026-09-09 kl. 05:36.
Citera
2026-09-09, 05:11
  #98
Medlem
cookieboys avatar
Absolut jag håller med om att det borde finnas.
Citera
2026-09-10, 12:35
  #99
Medlem
The Crashs avatar
Citat:
Ursprungligen postat av The Crash
Cappade limits på min implementerare. Har inte anslutit ChatGPT/ Codex mot min git-instans ännu. Ledsen att jag inte kan släppa källkoden riktigt ännu. Det kommer. Men källkoden måste härdas och så vill jag inte släppa all dokumentation ännu. Den härdas via Sonarqube / Sonarcloud. Så återkommer när det är löst.

Men här är en beständig länk:
https://typ.gegge.se/flashbackaren
Och här kommer källkoden. Inte exakt allt. Men det relevanta. Mesta som är borta är mina pekare för att AI:n ska omsätta commits rätt.

https://github.com/GeGGe01/flashbackaren-public

Och här har ni något vackert. Hur man differentierar mellan visitors utan att kräva inloggning

Kod:
// src/worker.ts
const COOKIE = 'fb_meta';
const TEN_YEARS = 60 * 60 * 24 * 365 * 10;
// One place to upsert a member. Partial facts don't clobber existing ones (COALESCE), so a
// moderators-page hit (no posts) won't wipe a post count learned from the memberlist, etc.
async function upsertMember(
  env: Env,
  m: { uid: string; username: string; regDate?: string | null; posts?: number | null; sourceUrl?: string | null },
): Promise<void> {
  const now = new Date().toISOString();
  await env.DB.prepare(
    `INSERT INTO members (uid, username, first_seen, last_seen, reg_date, posts, source_url)
       VALUES (?1, ?2, ?3, ?3, ?4, ?5, ?6)
     ON CONFLICT (uid) DO UPDATE SET
       username   = excluded.username,
       last_seen  = excluded.last_seen,
       reg_date   = COALESCE(excluded.reg_date, members.reg_date),
       posts      = COALESCE(excluded.posts, members.posts),
       source_url = COALESCE(excluded.source_url, members.source_url)`,
  )
    .bind(m.uid, m.username, now, m.regDate ?? null, m.posts ?? null, m.sourceUrl ?? null)
    .run();
}

// Resolve an exact username via Flashback's PUBLIC member search: sok/?query=p:<name> 302-redirects
// to the profile /uNNN on an exact match. Bytes cross the normalizer; we parse UTF-8 facts only.
// Distinguishes NOT_FOUND (no exact match) from ERROR (upstream/rate-limit/parse failure) internally,
// so an error is never cached as a miss; the external API response stays minimal regardless.
type MemberRow = { uid: string; username: string; avg_bp: number | null; cnt: number };
type ResolveResult = { status: 'found'; member: MemberRow } | { status: 'not_found' } | { status: 'error' };
async function resolveMemberUpstream(q: string, env: Env): Promise<ResolveResult> {
  let search;
  try {
    search = await fetchFlashback(
      'https://www.flashback.org/sok/?query=' + encodeURIComponent('p:' + q),
      { redirect: 'manual' },
    );
  } catch {
    return { status: 'error' };
  }
  const um = (search.location ?? '').match(/\/u(\d+)/);
  if (!um) return { status: 'not_found' }; // no exact-match redirect = genuinely not found
  const uid = um[1];
  try {
    const profileUrl = 'https://www.flashback.org/u' + uid;
    const profile = await fetchFlashback(profileUrl);
    const tm = profile.html.match(/Visa profil:\s*([^<]+)</);
    const username = (tm ? decodeEntities(tm[1]) : q).trim();
    if (!username) return { status: 'error' }; // redirected to a profile we couldn't parse
    // Reg date is in the static profile HTML; post count is public but JS-rendered (not here).
    const rm = profile.html.match(/Reg:\s*<strong>(\d{4}-\d{2}-\d{2})<\/strong>/);
    await upsertMember(env, { uid, username, regDate: rm ? rm[1] : null, sourceUrl: profileUrl });
    return { status: 'found', member: { uid, username, avg_bp: null, cnt: 0 } };
  } catch {
    return { status: 'error' };
  }
}

// fb_meta is a SIGNED cookie: `<meta_id>.<hmac>`. The meta_id is a public identifier (it shows up as
// author_identity_id); the HMAC — over the id with META_SIGNING_SECRET — is the proof that this
// browser owns it. Ownership/author checks therefore can't be passed by simply presenting someone
// else's public id. HttpOnly/Secure/SameSite as before.
async function hmacHex(secret: string, msg: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(msg));
  return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, '0')).join('');
}

// The cookie VALUE for a meta id. Without a signing secret we can't sign — the returned value won't
// verify on the next request (→ a fresh ephemeral identity each time), which is safe (no forgery) but
// loses persistent ownership; production must set META_SIGNING_SECRET.
async function signMetaCookie(env: Env, metaId: string): Promise<string> {
  if (!env.META_SIGNING_SECRET) return metaId;
  return `${metaId}.${await hmacHex(env.META_SIGNING_SECRET, metaId)}`;
}

// Return the meta id ONLY when the cookie carries a valid signature. Unsigned/tampered/missing → null.
async function readMeta(request: Request, env: Env): Promise<string | null> {
  const raw = request.headers.get('Cookie') ?? '';
  const m = raw.match(/(?:^|;\s*)fb_meta=([^;]+)/);
  if (!m) return null;
  const cookie = decodeURIComponent(m[1]);
  const dot = cookie.lastIndexOf('.');
  if (dot <= 0) return null; // unsigned prototype cookie → not authenticated (rotate to a signed one)
  const id = cookie.slice(0, dot);
  const sig = cookie.slice(dot + 1);
  if (!env.META_SIGNING_SECRET) return null; // can't verify → don't trust
  const expected = await hmacHex(env.META_SIGNING_SECRET, id);
  return timingSafeEqual(sig, expected) ? id : null;
}

function json(body: unknown, extraHeaders?: Record<string, string>, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'Content-Type': 'application/json', ...(extraHeaders ?? {}) },
  });
}
__________________
Senast redigerad av The Crash 2026-09-10 kl. 12:42.
Citera
  • 8
  • 9

Skapa ett konto eller logga in för att kommentera

Du måste vara medlem för att kunna kommentera

Skapa ett konto

Det är enkelt att registrera ett nytt konto

Bli medlem

Logga in

Har du redan ett konto? Logga in här

Logga in