Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ curl -s -X POST https://axionlabsai-lumen.hf.space/gradio_api/v1/chat/completion
# → should return JSON with choices, not 405 HTML

# Also test via CF Worker (full stack):
curl -s -X POST https://api.amplifiedsmp.org/v1/chat/completions \
curl -s -X POST https://api.sennoric.com/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"lumen","messages":[{"role":"user","content":"hi"}]}'
```
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The deployed public website is maintained separately in
### Install (recommended)

```bash
curl -fsSL https://axion.amplifiedsmp.org/install.sh | sh
curl -fsSL https://sennoric.com/install.sh | sh
```

Or directly via npm:
Expand Down
13 changes: 13 additions & 0 deletions api-proxy-cf/migrations/041_domain_migration_codes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Short-lived, one-time browser handoffs used only while moving authenticated
-- sessions from amplifiedsmp.org to sennoric.com.
CREATE TABLE domain_migration_codes (
code TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
redeemed_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_domain_migration_codes_expires
ON domain_migration_codes(expires_at);
Comment thread
Ravikxx marked this conversation as resolved.
6 changes: 3 additions & 3 deletions api-proxy-cf/src/chatGeneration.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ALLOWED_WEB_ORIGINS } from './webOrigins.js'

const COMPLETIONS_URL = 'https://api.amplifiedsmp.org/v1/chat/completions'
const COMPLETIONS_URL = 'https://api.sennoric.com/v1/chat/completions'

// Partial text is written to storage at most this often. Frequent enough that a
// reader attaching after an eviction sees almost everything, rare enough that a
Expand Down Expand Up @@ -30,7 +30,7 @@ async function sendEmail(resendKey, { to, subject, html }) {
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${resendKey}` },
body: JSON.stringify({ from: 'Sennoric <noreply@amplifiedsmp.org>', to: [to], subject, html }),
body: JSON.stringify({ from: 'Sennoric <noreply@sennoric.com>', to: [to], subject, html }),
})
if (!res.ok) {
const body = await res.text().catch(() => '')
Expand Down Expand Up @@ -63,7 +63,7 @@ async function notifyScheduledCompletion(env, job, { status, error }) {
html: `<div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:32px;background:#0f0f11;color:#e8e8f0">
<h2 style="margin:0 0 8px;color:#e8e8f0">"${name}" ${ok ? 'finished' : 'failed'}</h2>
${body}
<a href="https://axion.amplifiedsmp.org/chat" style="display:inline-block;background:#e8602c;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:700">Open Sennoric &rarr;</a>
<a href="https://sennoric.com/chat" style="display:inline-block;background:#e8602c;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:700">Open Sennoric &rarr;</a>
</div>`,
})
} catch (err) {
Expand Down
235 changes: 166 additions & 69 deletions api-proxy-cf/src/index.js

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions api-proxy-cf/src/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function labelFor(service) {
// the request would normally go through, with no network hop at all.
async function checkSennoricApi(env, appFetch) {
try {
const res = await appFetch(new Request('https://api.amplifiedsmp.org/v1/models'))
const res = await appFetch(new Request('https://api.sennoric.com/v1/models'))
return res.ok
? { service: 'axion_api', status: 'up', detail: '' }
: { service: 'axion_api', status: 'down', detail: `HTTP ${res.status}` }
Expand All @@ -45,7 +45,7 @@ async function checkLumen(env, fetchImpl) {

async function checkWebsite(env, fetchImpl) {
try {
const res = await fetchImpl('https://axion.amplifiedsmp.org/', { method: 'GET' })
const res = await fetchImpl('https://sennoric.com/', { method: 'GET' })
return res.ok
? { service: 'website', status: 'up', detail: '' }
: { service: 'website', status: 'down', detail: `HTTP ${res.status}` }
Expand Down Expand Up @@ -74,7 +74,7 @@ async function alertAdmin(env, { subject, html }) {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${env.RESEND_API_KEY}` },
body: JSON.stringify({
from: 'Sennoric Status <status@amplifiedsmp.org>',
from: 'Sennoric Status <status@sennoric.com>',
to: [env.ADMIN_ALERT_EMAIL],
subject,
html,
Expand Down Expand Up @@ -108,7 +108,7 @@ async function evaluateIncident(env, result, nowIso) {
'INSERT INTO status_incident_updates (id, incident_id, status, body, created_at) VALUES (?,?,?,?,?)'
).bind(crypto.randomUUID(), id, 'investigating', body, nowIso),
])
const editLink = `https://axion.amplifiedsmp.org/admin#incident-${id}`
const editLink = `https://sennoric.com/admin#incident-${id}`
await alertAdmin(env, {
subject: `[Sennoric Status] ${title}`,
html: emailWrap(`
Expand All @@ -131,7 +131,7 @@ async function evaluateIncident(env, result, nowIso) {
'INSERT INTO status_incident_updates (id, incident_id, status, body, created_at) VALUES (?,?,?,?,?)'
).bind(crypto.randomUUID(), existing.id, 'resolved', body, nowIso),
])
const editLink = `https://axion.amplifiedsmp.org/admin#incident-${existing.id}`
const editLink = `https://sennoric.com/admin#incident-${existing.id}`
await alertAdmin(env, {
subject: `[Sennoric Status] Resolved: ${existing.title}`,
html: emailWrap(`
Expand Down
9 changes: 3 additions & 6 deletions api-proxy-cf/src/webOrigins.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
// chatGeneration.js can both import it without a circular dependency
// (index.js is what imports ChatGeneration from chatGeneration.js).
//
// NEW_WEB_ORIGIN (sennoric.com) is not live yet as of this change --
// DNS/GitHub Pages cutover pending. Accepted here ahead of time so CORS
// doesn't need another deploy the moment it goes live.
export const WEB_ORIGIN = 'https://axion.amplifiedsmp.org'
export const NEW_WEB_ORIGIN = 'https://sennoric.com'
export const ALLOWED_WEB_ORIGINS = [WEB_ORIGIN, NEW_WEB_ORIGIN]
export const WEB_ORIGIN = 'https://sennoric.com'
export const LEGACY_WEB_ORIGIN = 'https://axion.amplifiedsmp.org'
export const ALLOWED_WEB_ORIGINS = [WEB_ORIGIN, LEGACY_WEB_ORIGIN]
53 changes: 51 additions & 2 deletions api-proxy-cf/test/billing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ class D1TestDatabase {
CREATE TABLE cloud_task_events (id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES cloud_tasks(id));
CREATE TABLE email_prefs (user_id TEXT PRIMARY KEY REFERENCES users(id));
CREATE TABLE device_codes (code TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id));
CREATE TABLE desktop_auth_codes (
code TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id)
);
CREATE TABLE desktop_integration_codes (
code TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id)
);
CREATE TABLE domain_migration_codes (
code TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id)
);
CREATE TABLE appeals (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
Expand Down Expand Up @@ -565,7 +577,7 @@ test('only an authenticated admin can manually run pending message review', asyn
assert.match(runResult.run_id, /^[0-9a-f-]{36}$/)
assert.equal(
runResult.details_url,
`https://axion.amplifiedsmp.org/admin-moderation?run=${runResult.run_id}`,
`https://sennoric.com/admin-moderation?run=${runResult.run_id}`,
)
const message = db.prepare(
'SELECT review_status, review_run_id FROM message_log WHERE id=1'
Expand Down Expand Up @@ -944,6 +956,9 @@ test('account deletion removes audits, rate limits, and every key in an owned or
db.prepare('INSERT INTO cloud_tasks (id, user_id) VALUES (?,?)').bind('member-cloud-task', 'member').run()
db.prepare('INSERT INTO cloud_task_events (id, task_id) VALUES (?,?)').bind('member-cloud-task-event', 'member-cloud-task').run()
db.prepare('INSERT INTO user_settings (user_id) VALUES (?)').bind('member').run()
db.prepare('INSERT INTO desktop_auth_codes (code, user_id) VALUES (?,?)').bind('desktop-code', 'member').run()
db.prepare('INSERT INTO desktop_integration_codes (code, user_id) VALUES (?,?)').bind('integration-code', 'member').run()
db.prepare('INSERT INTO domain_migration_codes (code, user_id) VALUES (?,?)').bind('migration-code', 'member').run()
db.prepare(
`INSERT INTO admin_account_edits
(id, user_id, admin_email, previous_plan, new_plan,
Expand Down Expand Up @@ -1009,6 +1024,9 @@ test('account deletion removes audits, rate limits, and every key in an owned or
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM cloud_tasks WHERE user_id=?').bind('member').first().count, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM cloud_task_events WHERE task_id=?').bind('member-cloud-task').first().count, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM user_settings WHERE user_id=?').bind('member').first().count, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM desktop_auth_codes WHERE user_id=?').bind('member').first().count, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM desktop_integration_codes WHERE user_id=?').bind('member').first().count, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM domain_migration_codes WHERE user_id=?').bind('member').first().count, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM admin_account_edits WHERE user_id=?').bind('member').first().count, 0)
assert.equal(db.prepare("SELECT COUNT(*) AS count FROM rate_limits WHERE key LIKE '%:member'").first().count, 0)
assert.equal(db.prepare("SELECT COUNT(*) AS count FROM rate_limits WHERE key='free:unrelated-ip'").first().count, 1)
Expand Down Expand Up @@ -1055,6 +1073,37 @@ function executionCtx() {
}
}

test('appeal notification links to the website admin panel', async () => {
const db = new D1TestDatabase()
addUser(db, 'appealing-user')
db.prepare(
'INSERT INTO appeals (id, user_id, email, token, status, created_at) VALUES (?,?,?,?,?,?)'
).bind('appeal-1', 'appealing-user', 'appealing-user@example.com', 'appeal-token', 'pending', Date.now()).run()

let emailRequest = null
const realFetch = globalThis.fetch
globalThis.fetch = async (url, options) => {
emailRequest = { url: String(url), body: JSON.parse(options.body) }
return Response.json({ id: 'email-1' })
}
try {
const { ctx, settle } = executionCtx()
const response = await app.request('/appeal/appeal-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: 'Please review this account.' }),
}, { DB: db, TOKEN_SECRET: 'appeal-secret', RESEND_API_KEY: 'resend-test' }, ctx)

assert.equal(response.status, 200)
await settle()
assert.equal(emailRequest.url, 'https://api.resend.com/emails')
assert.match(emailRequest.body.html, /href="https:\/\/sennoric\.com\/admin"/)
assert.doesNotMatch(emailRequest.body.html, /https:\/\/api\.sennoric\.com\/admin/)
} finally {
globalThis.fetch = realFetch
}
})

test('session-authenticated completions are charged to the account', async () => {
const db = new D1TestDatabase()
const secret = 'session-billing-secret'
Expand Down Expand Up @@ -1322,7 +1371,7 @@ test('chat completions require an account and never call the model for anonymous
const body = await response.json()
assert.equal(body.error.type, 'authentication_error')
assert.equal(body.error.signup_required, true)
assert.equal(body.error.signup_url, 'https://axion.amplifiedsmp.org/chat')
assert.equal(body.error.signup_url, 'https://sennoric.com/chat')
assert.equal(upstreamCalls, 0)
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM message_log').first().count, 0)
assert.equal(db.prepare("SELECT COUNT(*) AS count FROM rate_limits WHERE key LIKE 'free:%'").first().count, 0)
Expand Down
2 changes: 1 addition & 1 deletion api-proxy-cf/test/chat-generation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ test('the Durable Object appends the assistant reply and completes the job after
globalThis.fetch = realFetch
}

assert.equal(requestSeen.url, 'https://api.amplifiedsmp.org/v1/chat/completions')
assert.equal(requestSeen.url, 'https://api.sennoric.com/v1/chat/completions')
assert.equal(requestSeen.options.headers.Authorization, 'Bearer signed-job-token')
// The object owns the only model call, and it streams so watching tabs get
// tokens as they arrive rather than one block at the end.
Expand Down
88 changes: 88 additions & 0 deletions api-proxy-cf/test/desktop-auth.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Statement {
class D1TestDatabase {
constructor() {
this.database = new DatabaseSync(':memory:')
this.database.exec('PRAGMA foreign_keys=ON')
this.database.exec(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
Expand All @@ -45,6 +46,14 @@ class D1TestDatabase {
expires_at INTEGER NOT NULL,
redeemed_at INTEGER
);
CREATE TABLE domain_migration_codes (
code TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
redeemed_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id)
);
Comment thread
Ravikxx marked this conversation as resolved.
`)
}
prepare(sql) { return new Statement(this.database, sql) }
Expand Down Expand Up @@ -221,3 +230,82 @@ test('a password reset invalidates a token minted before it', async () => {
const response = await approve(env, minted, { code_challenge: (await pkcePair()).challenge })
assert.equal(response.status, 401)
})

test('the old HttpOnly session migrates through a signed single-use handoff', async () => {
const { env } = makeEnv()
const token = await sessionToken('u1')
const start = await app.request(
'https://api.amplifiedsmp.org/auth/domain-migrate?return=https%3A%2F%2Faxion.amplifiedsmp.org%2Fkeys%3Ftab%3Dusage',
{ headers: { Cookie: `axion_session=${token}` } },
env,
)

assert.equal(start.status, 302)
const acceptUrl = new URL(start.headers.get('location'))
assert.equal(acceptUrl.origin, 'https://api.sennoric.com')
assert.equal(acceptUrl.pathname, '/auth/domain-migrate/accept')
assert.ok(acceptUrl.searchParams.get('handoff'))
assert.equal(acceptUrl.searchParams.has('token'), false)

const attempts = await Promise.all([
app.request(acceptUrl.href, {}, env),
app.request(acceptUrl.href, {}, env),
])
assert.deepEqual(attempts.map(response => response.status).sort(), [302, 400])
const accepted = attempts.find(response => response.status === 302)
assert.equal(accepted.status, 302)
assert.equal(accepted.headers.get('location'), 'https://sennoric.com/keys?tab=usage')
assert.match(accepted.headers.get('set-cookie'), /Domain=\.sennoric\.com/)
assert.match(accepted.headers.get('set-cookie'), /HttpOnly/)
Comment thread
Ravikxx marked this conversation as resolved.
assert.match(accepted.headers.get('set-cookie'), /;\s*Secure(?:;|$)/i)

const replay = await app.request(acceptUrl.href, {}, env)
assert.equal(replay.status, 400)
})

test('an expired domain migration code cannot be accepted', async () => {
const { db, env } = makeEnv()
const token = await sessionToken('u1')
const start = await app.request(
'https://api.amplifiedsmp.org/auth/domain-migrate?return=%2Fkeys',
{ headers: { Cookie: `axion_session=${token}` } },
env,
)
const acceptUrl = new URL(start.headers.get('location'))
const signedState = acceptUrl.searchParams.get('handoff')
const state = JSON.parse(atob(signedState.split('.')[0]))
db.prepare('UPDATE domain_migration_codes SET expires_at=? WHERE code=?')
.bind(Date.now() - 1, state.code).run()

const response = await app.request(acceptUrl.href, {}, env)
assert.equal(response.status, 400)
})

test('domain migration without an old session redirects without minting a handoff', async () => {
const { env } = makeEnv()
const response = await app.request(
'https://api.amplifiedsmp.org/auth/domain-migrate?return=https%3A%2F%2Faxion.amplifiedsmp.org%2Fdocs',
{},
env,
)
assert.equal(response.status, 302)
assert.equal(response.headers.get('location'), 'https://sennoric.com/docs')
})

test('the old website preserves paths and routes account visits through the signed handoff', async () => {
const { env } = makeEnv()

const docs = await app.request('https://axion.amplifiedsmp.org/docs?section=cli', {}, env)
assert.equal(docs.status, 302)
assert.equal(docs.headers.get('location'), 'https://sennoric.com/docs?section=cli')

const keys = await app.request('https://axion.amplifiedsmp.org/keys?tab=usage', {}, env)
assert.equal(keys.status, 302)
const migrate = new URL(keys.headers.get('location'))
assert.equal(migrate.origin, 'https://api.amplifiedsmp.org')
assert.equal(migrate.pathname, '/auth/domain-migrate')
assert.equal(
migrate.searchParams.get('return'),
'https://sennoric.com/keys?tab=usage&domain_migration=checked',
)
})
4 changes: 2 additions & 2 deletions api-proxy-cf/test/desktop-integrations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ test('GitHub integration OAuth is brokered with PKCE and the provider token is s

mock.method(globalThis, 'fetch', async (input, init) => {
assert.equal(String(input), 'https://github.com/login/oauth/access_token')
assert.equal(JSON.parse(init.body).redirect_uri, 'https://api.amplifiedsmp.org/auth/github/callback')
assert.equal(JSON.parse(init.body).redirect_uri, 'https://api.sennoric.com/auth/github/callback')
return Response.json({ access_token: 'gho_secret', scope: 'repo read:user', token_type: 'bearer' })
})
const callback = await app.request(`/auth/github/callback?code=provider-code&state=${encodeURIComponent(authorizationUrl.searchParams.get('state'))}`, {}, env)
Expand Down Expand Up @@ -151,7 +151,7 @@ test('Notion integration OAuth is brokered without exposing its client secret',
mock.method(globalThis, 'fetch', async (input, init) => {
assert.equal(String(input), 'https://api.notion.com/v1/oauth/token')
assert.equal(init.headers.Authorization, `Basic ${btoa('notion-client:notion-secret')}`)
assert.equal(JSON.parse(init.body).redirect_uri, 'https://api.amplifiedsmp.org/auth/notion/callback')
assert.equal(JSON.parse(init.body).redirect_uri, 'https://api.sennoric.com/auth/notion/callback')
return Response.json({ access_token: 'secret_notion', workspace_name: 'Sennoric' })
})
const callback = await app.request(`/auth/notion/callback?code=provider-code&state=${encodeURIComponent(authorizationUrl.searchParams.get('state'))}`, {}, env)
Expand Down
15 changes: 15 additions & 0 deletions api-proxy-cf/test/migrations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,18 @@ test('user_settings accepts the same TEXT ids used by users after migration 038'
userId,
)
})

test('domain migration codes insert with a user association and start unredeemed', () => {
const db = new DatabaseSync(':memory:')
db.exec('CREATE TABLE users (id TEXT PRIMARY KEY)')
db.exec(migration('041_domain_migration_codes.sql'))
db.prepare('INSERT INTO users (id) VALUES (?)').run('u1')
db.prepare(`
INSERT INTO domain_migration_codes (code, user_id, created_at, expires_at)
VALUES (?, ?, ?, ?)
`).run('code', 'u1', 1, 2)

const row = db.prepare('SELECT * FROM domain_migration_codes WHERE code=?').get('code')
assert.equal(row.user_id, 'u1')
assert.equal(row.redeemed_at, null)
})
2 changes: 1 addition & 1 deletion api-proxy-cf/test/sandbox-route.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ test('anonymous requests (no auth header at all) require signup and are rejected
const body = await response.json()
assert.equal(body.error.type, 'authentication_error')
assert.equal(body.error.signup_required, true)
assert.equal(body.error.signup_url, 'https://axion.amplifiedsmp.org/chat')
assert.equal(body.error.signup_url, 'https://sennoric.com/chat')
})

test('a banned session-token account is rejected (requireAuth already filters banned users to null, same as /v1/chat/completions — so this is a 401, not a 403)', async () => {
Expand Down
2 changes: 1 addition & 1 deletion api-proxy-cf/test/status.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function fetchStub({ axionApiUp = true, lumenUp = true, websiteUp = true } = {})
return async (url) => {
const s = typeof url === 'string' ? url : url.url
if (s.includes('runpod.ai')) return { ok: lumenUp }
if (s.includes('api.amplifiedsmp.org')) return { ok: axionApiUp }
if (s.includes('api.sennoric.com')) return { ok: axionApiUp }
return { ok: websiteUp }
}
}
Expand Down
8 changes: 8 additions & 0 deletions api-proxy-cf/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ new_sqlite_classes = ["BridgeRelay"]
tag = "v2"
new_sqlite_classes = ["ChatGeneration"]

[[routes]]
pattern = "api.sennoric.com/*"
zone_name = "sennoric.com"

[[routes]]
pattern = "api.amplifiedsmp.org/*"
zone_name = "amplifiedsmp.org"

[[routes]]
pattern = "axion.amplifiedsmp.org/*"
zone_name = "amplifiedsmp.org"

[triggers]
crons = ["*/5 * * * *", "0 * * * *", "* * * * *"]
Loading
Loading