commit 5e1adfab6063ecfc00aca9073a95ca09c8fe27b0 Author: Aaron Bieber Date: Fri Apr 19 07:04:46 2024 -0600 initial diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d187fc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.direnv +*.bak +result +tags +starpub diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5718c90 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Aaron Bieber + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/data/db.go b/data/db.go new file mode 100644 index 0000000..7a811cf --- /dev/null +++ b/data/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.23.0 + +package data + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/data/models.go b/data/models.go new file mode 100644 index 0000000..0140080 --- /dev/null +++ b/data/models.go @@ -0,0 +1,47 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.23.0 + +package data + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Category struct { + ID int32 `json:"id"` + UserID int32 `json:"user_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + Name string `json:"name"` + Descr string `json:"descr"` +} + +type Entry struct { + ID int32 `json:"id"` + UserID int32 `json:"user_id"` + Pubic pgtype.Bool `json:"pubic"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + Title string `json:"title"` + Descr string `json:"descr"` + Signature string `json:"signature"` +} + +type EntryCategory struct { + UserID int32 `json:"user_id"` + EntryID int32 `json:"entry_id"` + CategoryID int32 `json:"category_id"` +} + +type User struct { + ID int32 `json:"id"` + Uid pgtype.UUID `json:"uid"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + RealName string `json:"real_name"` + Username string `json:"username"` + Hash string `json:"hash"` + Email string `json:"email"` + Pubkey []byte `json:"pubkey"` +} diff --git a/data/queries.sql.go b/data/queries.sql.go new file mode 100644 index 0000000..5a90a1a --- /dev/null +++ b/data/queries.sql.go @@ -0,0 +1,308 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.23.0 +// source: queries.sql + +package data + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createEntry = `-- name: CreateEntry :one +INSERT INTO entries ( + user_id, title, descr +) VALUES ( + $1, $2, $3 +) +RETURNING id, created_at, to_tsvector(descr) +` + +type CreateEntryParams struct { + UserID int32 `json:"user_id"` + Title string `json:"title"` + Descr string `json:"descr"` +} + +type CreateEntryRow struct { + ID int32 `json:"id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + ToTsvector interface{} `json:"to_tsvector"` +} + +func (q *Queries) CreateEntry(ctx context.Context, arg CreateEntryParams) (CreateEntryRow, error) { + row := q.db.QueryRow(ctx, createEntry, arg.UserID, arg.Title, arg.Descr) + var i CreateEntryRow + err := row.Scan(&i.ID, &i.CreatedAt, &i.ToTsvector) + return i, err +} + +const createUser = `-- name: CreateUser :one +INSERT INTO users ( + real_name, username, pubkey +) VALUES ( + $1, $2, $3 +) +RETURNING id, username, pubkey +` + +type CreateUserParams struct { + RealName string `json:"real_name"` + Username string `json:"username"` + Pubkey []byte `json:"pubkey"` +} + +type CreateUserRow struct { + ID int32 `json:"id"` + Username string `json:"username"` + Pubkey []byte `json:"pubkey"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) { + row := q.db.QueryRow(ctx, createUser, arg.RealName, arg.Username, arg.Pubkey) + var i CreateUserRow + err := row.Scan(&i.ID, &i.Username, &i.Pubkey) + return i, err +} + +const getEntryCategories = `-- name: GetEntryCategories :many +SELECT + id, user_id, created_at, updated_at, name, descr +FROM + categories +WHERE + id IN ( + SELECT + category_id + FROM + entry_categories a + WHERE + a.entry_id = $1 + AND a.user_id = $2 + ) +` + +type GetEntryCategoriesParams struct { + EntryID int32 `json:"entry_id"` + UserID int32 `json:"user_id"` +} + +func (q *Queries) GetEntryCategories(ctx context.Context, arg GetEntryCategoriesParams) ([]Category, error) { + rows, err := q.db.Query(ctx, getEntryCategories, arg.EntryID, arg.UserID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Category{} + for rows.Next() { + var i Category + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Descr, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUser = `-- name: GetUser :one +SELECT + id, uid, created_at, updated_at, real_name, username, hash, email, pubkey +FROM + users +WHERE + id = $1 +LIMIT + 1 +` + +func (q *Queries) GetUser(ctx context.Context, id int32) (User, error) { + row := q.db.QueryRow(ctx, getUser, id) + var i User + err := row.Scan( + &i.ID, + &i.Uid, + &i.CreatedAt, + &i.UpdatedAt, + &i.RealName, + &i.Username, + &i.Hash, + &i.Email, + &i.Pubkey, + ) + return i, err +} + +const getUserByName = `-- name: GetUserByName :one +SELECT + id, uid, created_at, updated_at, real_name, username, hash, email, pubkey +FROM + users +WHERE + username = $1 +LIMIT + 1 +` + +func (q *Queries) GetUserByName(ctx context.Context, username string) (User, error) { + row := q.db.QueryRow(ctx, getUserByName, username) + var i User + err := row.Scan( + &i.ID, + &i.Uid, + &i.CreatedAt, + &i.UpdatedAt, + &i.RealName, + &i.Username, + &i.Hash, + &i.Email, + &i.Pubkey, + ) + return i, err +} + +const getUserCategories = `-- name: GetUserCategories :many +SELECT + id, user_id, created_at, updated_at, name, descr +FROM + categories +WHERE + user_id = $1 +` + +func (q *Queries) GetUserCategories(ctx context.Context, userID int32) ([]Category, error) { + rows, err := q.db.Query(ctx, getUserCategories, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Category{} + for rows.Next() { + var i Category + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Descr, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserEntries = `-- name: GetUserEntries :many +SELECT + id, user_id, pubic, created_at, updated_at, title, descr, signature +FROM + entries +WHERE + user_id = $1 +` + +func (q *Queries) GetUserEntries(ctx context.Context, userID int32) ([]Entry, error) { + rows, err := q.db.Query(ctx, getUserEntries, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Entry{} + for rows.Next() { + var i Entry + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Pubic, + &i.CreatedAt, + &i.UpdatedAt, + &i.Title, + &i.Descr, + &i.Signature, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const similarEntries = `-- name: SimilarEntries :many +SELECT + id, + similarity(descr, $2) AS similarity, + ts_headline( + 'english', + descr, + q, + 'StartSel = , StopSel = ' + ) :: text AS headline, + title +FROM + entries, + to_tsquery($2) q +WHERE + user_id = $1 + AND similarity(descr, $2) > 0.0 + AND similarity(descr, $2) < 1.0 +ORDER BY + similarity DESC +LIMIT + 10 +` + +type SimilarEntriesParams struct { + UserID int32 `json:"user_id"` + Similarity string `json:"similarity"` +} + +type SimilarEntriesRow struct { + ID int32 `json:"id"` + Similarity float32 `json:"similarity"` + Headline string `json:"headline"` + Title string `json:"title"` +} + +func (q *Queries) SimilarEntries(ctx context.Context, arg SimilarEntriesParams) ([]SimilarEntriesRow, error) { + rows, err := q.db.Query(ctx, similarEntries, arg.UserID, arg.Similarity) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SimilarEntriesRow{} + for rows.Next() { + var i SimilarEntriesRow + if err := rows.Scan( + &i.ID, + &i.Similarity, + &i.Headline, + &i.Title, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..0b791c7 --- /dev/null +++ b/extension/background.js @@ -0,0 +1,27 @@ +import * as Starpub from "./starpub.js"; + +document.addEventListener("DOMContentLoaded", function () { + let starKey = new Starpub(); + + browser.browserAction.onClicked.addListener(function () { + let maybeKey = starKey.getKey(); + maybeKey.then((_) => { + let tab = browser.tabs.query({ active: true, currentWindow: true }); + tab.then(function (a) { + const t = a[0]; + const st = starKey.sign({ + url: t.url, + title: t.title, + }); + st.then((p) => { + const vfy = Coze.Verify(p, starKey.pubKey); + vfy.then((v) => { + console.log("signature verified: ", v); + console.log(t.url, t.title); + console.log(starKey.pubKey); + }); + }); + }); + }); + }); +}); diff --git a/extension/coze.min.js b/extension/coze.min.js new file mode 100644 index 0000000..ea21c61 --- /dev/null +++ b/extension/coze.min.js @@ -0,0 +1,2 @@ +var t={UnknownAlg:"UnknownAlg",ES224:"ES224",ES256:"ES256",ES384:"ES384",ES512:"ES512",Ed25519:"Ed25519",Ed25519ph:"Ed25519ph",Ed448:"Ed448",SHA224:"SHA-224",SHA256:"SHA-256",SHA384:"SHA-384",SHA512:"SHA-512",SHA3224:"SHA3-224",SHA3256:"SHA3-256",SHA3384:"SHA3-384",SHA3512:"SHA3-512",SHAKE128:"SHAKE128",SHAKE256:"SHAKE256"},d={EC:"EC",SHA:"SHA",RSA:"RSA"},o={ECDSA:"ECDSA",EdDSA:"EdDSA",SHA2:"SHA2",SHA3:"SHA3"},u={P224:"P-224",P256:"P-256",P384:"P-384",P521:"P-521",Curve25519:"Curve25519",Curve448:"Curve448"},H={Sig:"sig",Enc:"enc",Hsh:"hsh"};function P(r){let e={};e.Name=r,e.Genus=y(r),e.Family=X(r),e.Use=J(r),e.Hash=l(r),e.HashSize=Z(r),e.HashSizeB64=Math.ceil(4*e.HashSize/3);try{e.XSize=T(r),e.XSizeB64=Math.ceil(4*e.XSize/3),e.DSize=_(r),e.DSizeB64=Math.ceil(4*e.DSize/3),e.Curve=B(r),e.SigSize=E(r),e.SigSizeB64=Math.ceil(4*e.SigSize/3)}catch{}return e}function y(r){switch(r){case t.ES224:case t.ES256:case t.ES384:case t.ES512:return o.ECDSA;case t.Ed25519:case t.Ed25519ph:case t.Ed448:return o.EdDSA;case t.SHA224:case t.SHA256:case t.SHA384:case t.SHA512:return o.SHA2;case t.SHA3224:case t.SHA3256:case t.SHA3384:case t.SHA3512:case t.SHAKE128:case t.SHAKE256:return o.SHA3;default:throw new Error("alg.Genus: unsupported algorithm: "+r)}}function X(r){switch(r){case t.ES224:case t.ES256:case t.ES384:case t.ES512:case t.Ed25519:case t.Ed25519ph:case t.Ed448:return d.EC;case t.SHA224:case t.SHA256:case t.SHA384:case t.SHA512:case t.SHA3224:case t.SHA3256:case t.SHA3384:case t.SHA3512:case t.SHAKE128:case t.SHAKE256:return d.SHA;default:throw new Error("alg.Family: unsupported algorithm: "+r)}}function l(r){switch(r){case t.ES224:case t.SHA224:return t.SHA224;case t.SHA256:case t.ES256:return t.SHA256;case t.SHA384:case t.ES384:return t.SHA384;case t.SHA512:case t.ES512:case t.Ed25519:case t.Ed25519ph:return t.SHA512;case t.SHAKE128:return t.SHAKE128;case t.SHAKE256:case t.Ed448:return t.SHAKE256;case t.SHA3224:return t.SHA3224;case t.SHA3256:return t.SHA3256;case t.SHA3384:return t.SHA3384;case t.SHA3512:return t.SHA3512;default:throw new Error("alg.HashAlg: unsupported algorithm: "+r)}}function Z(r){switch(l(r)){case t.SHA224:case t.SHA3224:return 28;case t.SHA256:case t.SHA3256:case t.SHAKE128:return 32;case t.SHA384:case t.SHA3384:return 48;case t.SHA512:case t.SHA3512:case t.SHAKE256:return 64;default:throw new Error("alg.HashSize: unsupported algorithm: "+r)}}function E(r){switch(r){case t.ES224:return 56;case t.ES256:case t.Ed25519:case t.Ed25519ph:return 64;case t.ES384:return 96;case t.Ed448:return 114;case t.ES512:return 132;default:throw new Error("alg.SigSize: unsupported algorithm: "+r)}}function T(r){switch(r){case t.Ed25519:case t.Ed25519ph:return 32;case t.ES224:return 56;case t.Ed448:return 57;case t.ES256:return 64;case t.ES384:return 96;case t.ES512:return 132;default:throw new Error("alg.XSize: unsupported algorithm: "+r)}}function _(r){switch(r){case t.ES224:return 28;case t.ES256:case t.Ed25519:case t.Ed25519ph:return 32;case t.ES384:return 48;case t.Ed448:return 57;case t.ES512:return 66;default:throw new Error("alg.DSize: unsupported algorithm: "+r)}}function B(r){switch(r){default:throw new Error("alg.Curve: unsupported algorithm: "+r);case t.ES224:return u.P224;case t.ES256:return u.P256;case t.ES384:return u.P384;case t.ES512:return u.P521;case t.Ed25519:case t.Ed25519ph:return u.Curve25519;case t.Ed448:return u.Curve448}}function J(r){switch(y(r)){default:throw new Error("alg.Use: unsupported algorithm: "+r);case o.EdDSA:case o.ECDSA:return H.Sig;case o.SHA2:case o.SHA3:return H.Hsh}}var w={ES224:BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D"),ES256:BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),ES384:BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),ES512:BigInt("0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409")},W={ES224:w.ES224>>BigInt(1),ES256:w.ES256>>BigInt(1),ES384:w.ES384>>BigInt(1),ES512:w.ES512>>BigInt(1)};function I(r){switch(r){default:throw new Error("CurveOrder: unsupported curve: "+r);case"ES224":case"ES256":case"ES384":case"ES512":return w[r]}}function U(r){switch(r){default:throw new Error("CurveHalfOrder: unsupported curve: "+r);case"ES224":case"ES256":case"ES384":case"ES512":return W[r]}}var s={New:async function(r){switch(i(r)&&(r=t.ES256),r){case t.ES256:case t.ES384:case t.ES512:return await window.crypto.subtle.generateKey({name:o.ECDSA,namedCurve:B(r)},!0,["sign","verify"]);default:throw new Error("CryptoKey.New: Unsupported key algorithm:"+r)}},FromCozeKey:async function(r,e){if(y(r.alg)!=o.ECDSA)throw new Error("CryptoKey.FromCozeKey: unsupported CryptoKey algorithm: "+r.alg);var a={};a.use=H.Sig,a.crv=B(r.alg),a.kty=d.EC;let n=T(r.alg)/2,f=await C(r.x);if(a.x=await c(f.slice(0,n)),a.y=await c(f.slice(n)),i(r.d)||e)var S="verify";else S="sign",a.d=r.d;return await crypto.subtle.importKey("jwk",a,{name:o.ECDSA,namedCurve:a.crv},!0,[S])},ToPublic:async function(r){delete r.d,r.key_ops=["verify"]},ToCozeKey:async function(r){let e=await window.crypto.subtle.exportKey("jwk",r);var a={};a.alg=await s.algFromCrv(e.crv);let n=C(e.x),f=C(e.y);var S=new Uint8Array([...n,...f]);return a.x=c(S.buffer),e.hasOwnProperty("d")&&(a.d=e.d),a.tmb=await F(a),a},SignBuffer:async function(r,e){let a=await s.algFromCrv(r.algorithm.namedCurve),n=await window.crypto.subtle.sign({name:o.ECDSA,hash:{name:l(a)}},r,e);return n=N(a,n),n},SignBufferB64:async function(r,e){return await c(await s.SignBuffer(r,e))},SignString:async function(r,e){return await s.SignBufferB64(r,await A(e))},VerifyArrayBuffer:async function(r,e,a,n){return await Y(r,n)?(await s.ToPublic(e),await window.crypto.subtle.verify({name:o.ECDSA,hash:{name:await s.GetSignHashAlgoFromCryptoKey(e)}},e,n,a)):!1},VerifyMsg:async function(r,e,a,n){return s.VerifyArrayBuffer(r,e,await A(a),await D(n))},GetSignHashAlgoFromCryptoKey:async function(r){return l(await s.algFromCrv(r.algorithm.namedCurve))},algFromCrv:async function(r){switch(r){case u.P224:var e=t.ES224;break;case u.P256:e=t.ES256;break;case u.P384:e=t.ES384;break;case u.P521:e=t.ES512;break;default:throw new Error("CryptoKey.ToCozeKey: Unsupported key algorithm.")}return e}};function z(r,e){if(typeof e!="bigint")throw new Error("IsLowS: s is not of type bigint");return U(r)>e}function Q(r,e){if(typeof e!="bigint")throw new Error("toLowS: s is not of type bigint");return z(r,e)?e:I(r)-e}async function sr(r,e){let a=await D(e),n=await N(r,a);return c(n)}async function Y(r,e){let a=await $(r,e);return z(r,a)}function $(r,e){let a=E(r)/2,n=e.slice(a);return O(n)}async function N(r,e){let a=E(r)/2,n=e.slice(0,a),f=e.slice(a),S=O(f),m=Q(r,S),h=q(E(r)/2,m);var g=new Uint8Array(n.byteLength+h.byteLength);return g.set(new Uint8Array(n),0),g.set(new Uint8Array(h),n.byteLength),e=g.buffer,e}function O(r){let e=0n,a=new Uint8Array(r);for(let n=0;n>=8n;while(r>0);return a}var rr=["alg","x"];async function ur(r){if(i(r)&&(r=t.ES256),y(r)==o.ECDSA)var e=await s.New(r);else throw new Error("Coze.NewKey: only ECDSA algs are currently supported.");let a=await s.ToCozeKey(e.privateKey);return a.iat=Math.floor(Date.now()/1e3),a.tmb=await F(a),a.kid="My Cyphr.me Key.",a}async function F(r){if(i(r.alg)||i(r.x))throw new Error("Coze.Thumbprint: alg or x is empty.");return p(r,await l(r.alg),rr)}async function fr(r){if(i(r.d))return console.error("Coze key missing `d`"),!1;try{let e="7AtyaCHO2BAG06z0W1tOQlZFWbhxGgqej4k9-HWP3DE-zshRbrE-69DIfgY704_FDYez7h_rEI1WQVKhv5Hd5Q",a=await v(e,r);return k(e,r,a)}catch{return!1}}async function Sr(r){if(typeof r!="object")return console.error("Correct: CozeKey must be passed in as an object."),!1;if(i(r.alg))return console.error("Correct: Alg must be set"),!1;let e=P(r.alg),a=i(r.tmb),n=i(r.x),f=i(r.d);if(a&&n&&f)return console.error("Correct: At least one of [x, tmb, d] must be set"),!1;if(n&&f)return a||r.tmb.length!==e.HashSizeB64?(console.error("Correct: Incorrect `tmb` size: ",r.tmb.length),!1):!0;if(!n&&r.x.length!==e.XSizeB64)return console.error("Correct: Incorrect x size: ",r.x.length),!1;if(!a&&!n){let S=await F(r);if(r.tmb!==S)return console.error("Correct: Incorrect given `tmb`: ",r.tmb),!1}if(!f&&!n){let S=await s.FromCozeKey(r),m=await A("Test Signing"),h=await s.SignBuffer(S,m),g=await s.FromCozeKey(r,!0);if(!await s.VerifyArrayBuffer(r.alg,g,m,h))return console.error("Correct: private key invalid."),!1}return!0}async function lr(r,e){if(i(r))throw new Error("CozeKey.Revoke: Private key not set. Cannot sign message");var a={};a.pay={},i(e)||(a.pay.msg=e),a.pay.rvk=Math.round(Date.now()/1e3);let n=r.rvk;return delete r.rvk,a=await V(a,r),n!==void 0?r.rvk=n:r.rvk=a.pay.rvk,a}function x(r){return!(i(r.rvk)||!(parseInt(r.rvk)>0))}var cr=["alg","iat","tmb","typ"];async function V(r,e,a){if(console.log(),x(e))throw new Error("SignCoze: Cannot sign with revoked key.");return r.pay.alg=e.alg,r.pay.tmb=await F(e),r.pay.iat=Math.round(Date.now()/1e3),i(a)||(r.pay=await b(r.pay,a)),r.sig=await v(JSON.stringify(r.pay),e),r}async function v(r,e){return s.SignBufferB64(await s.FromCozeKey(e),await A(r))}async function Ar(r,e,a){if(x(e))throw new Error("SignCozeRaw: Cannot sign with revoked key.");if(!i(r.pay.alg)&&r.pay.alg!==e.alg)throw new Error("SignCozeRaw: Coze key alg mismatch with coze.pay.alg.");if(!i(r.pay.tmb)&&r.pay.tmb!==e.tmb)throw new Error("SignCozeRaw: Coze key tmb mismatch with coze.pay.tmb.");return i(a)||(r.pay=await b(r.pay,a)),r.sig=await v(JSON.stringify(r.pay),e),r}async function yr(r,e){if(!i(r.pay.alg)&&r.pay.alg!==e.alg)throw new Error("VerifyCoze: Coze key alg mismatch with coze.pay.alg.");if(!i(r.pay.tmb)&&r.pay.tmb!==e.tmb)throw new Error("VerifyCoze: Coze key tmb mismatch with coze.pay.tmb.");return k(JSON.stringify(r.pay),e,r.sig)}async function k(r,e,a){return s.VerifyMsg(e.alg,await s.FromCozeKey(e,!0),r,a)}async function Fr(r,e){if(i(r.pay))throw new Error("Meta: coze.pay must exist.");let a={};if(i(e)){if(i(r.pay.alg))throw new Error("Meta: either coze.pay.alg or parameter alg must be set.");a.alg=r.pay.alg}else a.alg=e;if(!i(r.pay.alg)&&a.alg!==r.pay.alg)throw new Error(`Meta: coze.pay.alg (${r.pay.alg}) and parameter alg (${e}) do not match. `);return i(r.pay.iat)||(a.iat=r.pay.iat),i(r.pay.tmb)||(a.tmb=r.pay.tmb),i(r.pay.typ)||(a.typ=r.pay.typ),a.can=await L(r.pay),a.cad=await p(r.pay,l(a.alg)),i(r.sig)||(a.sig=r.sig,a.czd=await p({cad:a.cad,sig:a.sig},l(a.alg))),a}async function A(r){return new TextEncoder().encode(r).buffer}function D(r){return C(r).buffer}function C(r){if(r=r.replace(/-/g,"+").replace(/_/g,"/"),btoa(atob(r)).replace(/=/g,"")!==r)throw new Error("Non-canonical base64 string");return Uint8Array.from(atob(r),a=>a.charCodeAt(0))}function c(r){return btoa(String.fromCharCode.apply(null,new Uint8Array(r))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function i(r){return typeof r=="function"?!1:Array.isArray(r)&&r.length==0?!0:r===Object(r)?Object.keys(r).length===0:!er(r)}function er(r){return!(r===!1||r==="false"||r===void 0||r==="undefined"||r===""||r===0||r==="0"||r===null||r==="null"||r==="NaN"||Number.isNaN(r)||r===Object(r))}function L(r){return Object.keys(r)}async function b(r,e){if(i(e))return r;let a={};for(let n of e)a[n]=r[n];return a}async function tr(r,e){return JSON.stringify(await b(r,e))}async function ar(r,e,a){if(i(e))throw new Error("Hash is not given");return await crypto.subtle.digest(e,await A(await tr(r,a)))}async function p(r,e,a){return await c(await ar(r,e,a))}export{t as Algs,c as ArrayBufferTo64ut,C as B64ToUint8Array,D as B64uToArrayBuffer,L as Canon,b as Canonical,ar as CanonicalHash,p as CanonicalHash64,tr as CanonicalS,Sr as Correct,s as CryptoKey,B as Curve,U as CurveHalfOrder,I as CurveOrder,u as Curves,_ as DSize,d as FamAlgs,X as Family,o as GenAlgs,y as Genus,l as HashAlg,Z as HashSize,x as IsRevoked,Y as IsSigLowS,Fr as Meta,ur as NewKey,P as Params,cr as PayCanon,lr as Revoke,A as SToArrayBuffer,E as SigSize,sr as SigToLowS,V as Sign,Ar as SignCozeRaw,v as SignPay,F as Thumbprint,rr as TmbCanon,J as Use,H as Uses,fr as Valid,yr as Verify,k as VerifyPay,T as XSize,i as isEmpty}; +//# sourceMappingURL=coze.min.js.map diff --git a/extension/icons/star-48.png b/extension/icons/star-48.png new file mode 100644 index 0000000..a0262ed Binary files /dev/null and b/extension/icons/star-48.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..7702de1 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,30 @@ +{ + "manifest_version": 2, + "name": "StarPub", + "version": "1.0", + + "browser_specific_settings": { + "gecko": { + "id": "starpub@starpuber" + } + }, + + "permissions": ["tabs", "storage"], + + "description": "Star anything", + + "icons": { + "48": "icons/star-48.png" + }, + + "browser_action": { + "default_icon": "icons/star-48.png", + "default_title": "Star it!", + "default_popup": "popup.html" + }, + + "background": { + "scripts": ["coze.min.js", "background.js"], + "type": "module" + } +} diff --git a/extension/popup.html b/extension/popup.html new file mode 100644 index 0000000..77e4143 --- /dev/null +++ b/extension/popup.html @@ -0,0 +1,20 @@ + + + + + + + + +
+
+ +
+ + +
+
+ + + + diff --git a/extension/popup.js b/extension/popup.js new file mode 100644 index 0000000..5c48a0a --- /dev/null +++ b/extension/popup.js @@ -0,0 +1,28 @@ +"use strict"; + +import * as Starpub from "./starpub.min.js"; + +let starKey = new Starpub(); +browser.browserAction.onClicked.addListener(function () { + let maybeKey = starKey.getKey(); + maybeKey.then((_) => { + let tab = browser.tabs.query({ active: true, currentWindow: true }); + tab.then(function (a) { + const t = a[0]; + const st = starKey.sign({ + url: t.url, + title: t.title, + }); + st.then((p) => { + const vfy = Coze.Verify(p, starKey.pubKey); + vfy.then((v) => { + let titleInp = document.getElementById("title"); + let descInp = document.getElementById("description"); + + console.log("signature verified: ", v, t); + titleInp.value = t.title; + }); + }); + }); + }); +}); diff --git a/extension/settings.html b/extension/settings.html new file mode 100644 index 0000000..77e4143 --- /dev/null +++ b/extension/settings.html @@ -0,0 +1,20 @@ + + + + + + + + +
+
+ +
+ + +
+
+ + + + diff --git a/extension/starpub.js b/extension/starpub.js new file mode 100644 index 0000000..1532781 --- /dev/null +++ b/extension/starpub.js @@ -0,0 +1,46 @@ +"use strict"; + +import * as Coze from "./coze.min.js"; + +export { Starpub }; + +class Starpub { + async getKey() { + let key = await browser.storage.local.get("starKey"); + if (Object.keys(key).length < 1) { + this.key = await Coze.NewKey("ES512"); + this.key.kid = "My StarPub Coze Key"; + await browser.storage.local.set({ starKey: this.key }); + } else { + this.key = key.starKey; + } + this.thumb = await Coze.Thumbprint(this.key); + const { d, ...pkRest } = this.key; + this.pubKey = pkRest; + } + + async sign(starObj) { + return await Coze.SignCozeRaw( + { + pay: { + url: starObj.url, + title: starObj.title, + typ: "starpub/msg/create", + + iat: Math.floor(Date.now() / 1000), + alg: this.key.alg, + tmb: this.key.tmb, + }, + }, + this.key, + ); + } + + async verify() { + return false; + } + + async publish() {} +} + + diff --git a/extension/starpub.png b/extension/starpub.png new file mode 100644 index 0000000..3f19e02 Binary files /dev/null and b/extension/starpub.png differ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3748dac --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1711668574, + "narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..60c9c0b --- /dev/null +++ b/flake.nix @@ -0,0 +1,57 @@ +{ + description = "starpub: star anything"; + + inputs.nixpkgs.url = "nixpkgs/nixos-23.11"; + + outputs = + { self + , nixpkgs + , + }: + let + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + in + { + overlay = _: prev: { inherit (self.packages.${prev.system}) starpub; }; + + packages = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + in + { + starpub = pkgs.buildGoModule { + pname = "starpub"; + version = "v0.0.0"; + src = ./.; + + vendorHash = pkgs.lib.fakeSha256; + }; + }); + + defaultPackage = forAllSystems (system: self.packages.${system}.starpub); + devShells = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + shellHook = '' + PS1='\u@\h:\@; ' + echo "Go `${pkgs.go}/bin/go version`" + ''; + nativeBuildInputs = with pkgs; [ + git + go + gopls + go-tools + sqlc + + (pkgs.callPackage (import ./ogen.nix) { }) + (postgresql.withPackages (p: [ p.postgis ])) + ]; + }; + }); + }; +} diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..6bb883b --- /dev/null +++ b/generate.go @@ -0,0 +1,4 @@ +package main + +//go:generate ogen --target petstore --clean openapi.yaml +//go:generate sqlc generate diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5375002 --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module suah.dev/starpub + +go 1.22.1 + +require ( + github.com/go-faster/errors v0.7.1 + github.com/go-faster/jx v1.1.0 + github.com/jackc/pgx/v5 v5.5.5 + github.com/ogen-go/ogen v1.0.0 + go.opentelemetry.io/otel v1.25.0 + go.opentelemetry.io/otel/metric v1.25.0 + go.opentelemetry.io/otel/trace v1.25.0 +) + +require ( + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-faster/yaml v0.4.6 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/segmentio/asm v1.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9d59264 --- /dev/null +++ b/go.sum @@ -0,0 +1,88 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= +github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg= +github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= +github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ogen-go/ogen v1.0.0 h1:n1hkgOnLtA1Xn369KAzJhqzphQzNo/wAI82NIaFQNXA= +github.com/ogen-go/ogen v1.0.0/go.mod h1:NFn616zR+/DPsq8rPoezaHlhKcNQzlYfo5gUieW8utI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e007c35 --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "log" + "net" + "net/http" + + "github.com/jackc/pgx/v5" + "suah.dev/starpub/data" +) + +var () + +func logger(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s\n", r.URL.Path) + f(w, r) + } +} + +func makeHandler(q *data.Queries, ctx context.Context) func(w http.ResponseWriter, r *http.Request) { + return nil +} + +func main() { + pd, err := pgx.Connect( + context.Background(), + "host=localhost dbname=postgres sslmode=disable password=''", + ) + if err != nil { + log.Fatal(err) + } + base := data.New(pd) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mux := http.NewServeMux() + mux.HandleFunc("/user/auth", logger(makeHandler(base, ctx))) + + mux.HandleFunc("POST /stars", logger(nil)) + mux.HandleFunc("DELETE /stars", logger(nil)) + + mux.HandleFunc("GET /{user}", logger(nil)) + mux.HandleFunc("GET /{user}.rss", logger(nil)) + + s := http.Server{ + Handler: mux, + } + + lis, err := net.Listen("tcp", ":8080") + if err != nil { + log.Fatal(err) + } + s.Serve(lis) +} diff --git a/ogen.nix b/ogen.nix new file mode 100644 index 0000000..40f16bf --- /dev/null +++ b/ogen.nix @@ -0,0 +1,26 @@ +{ lib +, buildGoModule +, fetchFromGitHub +, ... +}: +with lib; +buildGoModule rec { + pname = "ogen"; + version = "1.0.0"; + + src = fetchFromGitHub { + owner = "ogen-go"; + repo = pname; + rev = "v${version}"; + sha256 = "sha256-khUY8PQ12p0slleidysZiGWyZNX3XCxwD55lfRDCwew="; + }; + + vendorHash = "sha256-kHfp77jKPBt+EKAc52nXdqR4TJ3OVj7mqo4vfv2XelQ="; + + meta = { + description = "OpenAPI v3 code generator for go"; + homepage = "https://github.com/ogen-go/ogen"; + license = licenses.asl20; + maintainers = with maintainers; [ qbit ]; + }; +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..232be63 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: Starpub API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 1.0.0 + +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing + +paths: + '/{user}': + get: + summary: Returns a list of users. + description: Optional extended description in CommonMark or HTML. + parameters: + - name: user + in: path + required: true + description: username + schema: + type: string + responses: + "200": # status code + description: A JSON array of user names + content: + application/json: + schema: + type: array + items: + type: string diff --git a/queries.sql b/queries.sql new file mode 100644 index 0000000..0d02a5a --- /dev/null +++ b/queries.sql @@ -0,0 +1,90 @@ +-- name: GetUserCategories :many +SELECT + * +FROM + categories +WHERE + user_id = $1; + +-- name: GetUserEntries :many +SELECT + * +FROM + entries +WHERE + user_id = $1; + +-- name: CreateUser :one +INSERT INTO users ( + real_name, username, pubkey +) VALUES ( + $1, $2, $3 +) +RETURNING id, username, pubkey; + +-- name: SimilarEntries :many +SELECT + id, + similarity(descr, $2) AS similarity, + ts_headline( + 'english', + descr, + q, + 'StartSel = , StopSel = ' + ) :: text AS headline, + title +FROM + entries, + to_tsquery($2) q +WHERE + user_id = $1 + AND similarity(descr, $2) > 0.0 + AND similarity(descr, $2) < 1.0 +ORDER BY + similarity DESC +LIMIT + 10; + +-- name: GetEntryCategories :many +SELECT + * +FROM + categories +WHERE + id IN ( + SELECT + category_id + FROM + entry_categories a + WHERE + a.entry_id = $1 + AND a.user_id = $2 + ); + +-- name: GetUser :one +SELECT + * +FROM + users +WHERE + id = $1 +LIMIT + 1; + +-- name: GetUserByName :one +SELECT + * +FROM + users +WHERE + username = $1 +LIMIT + 1; + +-- name: CreateEntry :one +INSERT INTO entries ( + user_id, title, descr +) VALUES ( + $1, $2, $3 +) +RETURNING id, created_at, to_tsvector(descr); diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..1322887 --- /dev/null +++ b/schema.sql @@ -0,0 +1,48 @@ +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +DROP Table if exists entry_categories; +drop table if exists categories; +drop table if exists entries; +drop table if exists users; + +CREATE TABLE users ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + uid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE, + created_at timestamp NOT NULL DEFAULT NOW(), + updated_at timestamp, + real_name text NOT NULL, + username text NOT NULL UNIQUE, + hash text NOT NULL, + email text NOT NULL, + pubkey jsonb NOT NULL +); + +CREATE TABLE categories ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + created_at timestamp NOT NULL DEFAULT NOW(), + updated_at timestamp, + name text NOT NULL, + descr text NOT NULL +); + +CREATE TABLE entries ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + pubic bool DEFAULT TRUE, + created_at timestamp NOT NULL DEFAULT NOW(), + updated_at timestamp, + title text NOT NULL DEFAULT '', + descr text NOT NULL DEFAULT '', + signature text NOT NULL +); + +CREATE TABLE entry_categories ( + user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + entry_id INTEGER NOT NULL REFERENCES entries (id) ON DELETE CASCADE, + category_id INTEGER NOT NULL REFERENCES categories (id) ON DELETE CASCADE +); + +CREATE INDEX entry_trgm_idx ON entries USING gist (title, descr gist_trgm_ops); diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..899751d --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,12 @@ +version: 2 +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "queries.sql" + gen: + go: + sql_package: "pgx/v5" + package: "data" + out: "data" + emit_json_tags: true + emit_empty_slices: true