Release 1.0.8 with platform, security, and UI hardening.
Adds API filter registry, style theme registry, SW bitmask cache clear, KV namespacing, session expiry checks, accessibility improvements, and expanded test coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getProvider } from '../../platform/storage.js';
|
||||
import { api } from '../../platform/api.js';
|
||||
import { SECURITY_REQUEST_FILTER } from '../runtime/api-auth.js';
|
||||
import { SecurityPolicy } from './SecurityPolicy.js';
|
||||
import {
|
||||
AccountProfile,
|
||||
@@ -57,16 +58,27 @@ export class ApiSecurityPolicy extends SecurityPolicy {
|
||||
|
||||
async _request(path, options = {}, extra = {}) {
|
||||
const { authToken = null, trackActivity = true } = extra;
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (authToken) {
|
||||
headers.set('Authorization', `Bearer ${authToken}`);
|
||||
}
|
||||
|
||||
return this.client.requestJSON(path, {
|
||||
...options,
|
||||
trackActivity,
|
||||
headers: {
|
||||
...(options.headers || {}),
|
||||
...(authToken ? { Authorization: `Bearer ${authToken}` } : {})
|
||||
}
|
||||
skipRequestFilters: [SECURITY_REQUEST_FILTER],
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
async getRequestAuthorization(_requestContext, { session } = {}) {
|
||||
const token = session?.jwt_token || this.cache.session?.jwt_token || null;
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
return { scheme: 'Bearer', token };
|
||||
}
|
||||
|
||||
_applyBundle(bundle = {}) {
|
||||
this.cache.session = bundle.session ? new Session(bundle.session) : null;
|
||||
this.cache.user = bundle.user ? new User(bundle.user) : null;
|
||||
@@ -100,9 +112,10 @@ export class ApiSecurityPolicy extends SecurityPolicy {
|
||||
async authenticate(credentials = {}) {
|
||||
const username = credentials.username || credentials.email || '';
|
||||
const password = credentials.password || '';
|
||||
const basicToken = typeof btoa === 'function'
|
||||
? btoa(`${username}:${password}`)
|
||||
: Buffer.from(`${username}:${password}`, 'utf-8').toString('base64');
|
||||
if (typeof btoa !== 'function') {
|
||||
throw new Error('Basic authentication requires btoa in the current runtime');
|
||||
}
|
||||
const basicToken = btoa(`${username}:${password}`);
|
||||
const bundle = await this._request('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -462,6 +475,49 @@ export class ApiSecurityPolicy extends SecurityPolicy {
|
||||
}, { authToken: this.cache.session?.jwt_token });
|
||||
}
|
||||
|
||||
async listAccountSettings(userId) {
|
||||
await this._ensureSessionLoaded();
|
||||
if (!this.cache.user || this.cache.user.id !== userId) {
|
||||
throw new Error('Account settings are only available for the authenticated user');
|
||||
}
|
||||
const rows = await this._request('/account/settings', {
|
||||
method: 'GET'
|
||||
}, { authToken: this.cache.session?.jwt_token });
|
||||
return Array.isArray(rows) ? clone(rows) : [];
|
||||
}
|
||||
|
||||
async getAccountSetting(userId, key) {
|
||||
await this._ensureSessionLoaded();
|
||||
if (!this.cache.user || this.cache.user.id !== userId) {
|
||||
throw new Error('Account settings are only available for the authenticated user');
|
||||
}
|
||||
return clone(await this._request(`/account/settings/${encodeURIComponent(key)}`, {
|
||||
method: 'GET'
|
||||
}, { authToken: this.cache.session?.jwt_token }));
|
||||
}
|
||||
|
||||
async updateAccountSetting(userId, key, patch = {}) {
|
||||
await this._ensureSessionLoaded();
|
||||
if (!this.cache.user || this.cache.user.id !== userId) {
|
||||
throw new Error('Account settings are only available for the authenticated user');
|
||||
}
|
||||
return clone(await this._request(`/account/settings/${encodeURIComponent(key)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(patch)
|
||||
}, { authToken: this.cache.session?.jwt_token }));
|
||||
}
|
||||
|
||||
async deleteAccountSetting(userId, key) {
|
||||
await this._ensureSessionLoaded();
|
||||
if (!this.cache.user || this.cache.user.id !== userId) {
|
||||
throw new Error('Account settings are only available for the authenticated user');
|
||||
}
|
||||
return this._request(`/account/settings/${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE'
|
||||
}, { authToken: this.cache.session?.jwt_token });
|
||||
}
|
||||
|
||||
async grantPermit(permitData) {
|
||||
await this._ensureSessionLoaded();
|
||||
const created = await this._request('/admin/permits', {
|
||||
@@ -492,6 +548,20 @@ export class ApiSecurityPolicy extends SecurityPolicy {
|
||||
this._invalidateAdminCache();
|
||||
}
|
||||
|
||||
_buildPrincipals(user) {
|
||||
if (!user?.id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const principals = [{ principal_type: 'user', principal_id: user.id }];
|
||||
if (Array.isArray(user.role_ids)) {
|
||||
user.role_ids.forEach((roleId) => {
|
||||
principals.push({ principal_type: 'role', principal_id: roleId });
|
||||
});
|
||||
}
|
||||
return principals;
|
||||
}
|
||||
|
||||
_evaluateCached(rights, resourcePath) {
|
||||
if (!this.cache.user?.id || !this.cache.session?.jwt_token) {
|
||||
return {
|
||||
@@ -504,7 +574,14 @@ export class ApiSecurityPolicy extends SecurityPolicy {
|
||||
|
||||
const requestedRights = normalizeRightsInput(rights);
|
||||
const targetPath = resourcePath || '/';
|
||||
const matchingPermits = this.cache.permits.filter((permit) => pathMatches(permit.resource_path, targetPath));
|
||||
const principals = this._buildPrincipals(this.cache.user);
|
||||
const matchingPermits = this.cache.permits.filter((permit) => {
|
||||
const principalMatch = principals.some((principal) => (
|
||||
principal.principal_type === permit.principal_type
|
||||
&& principal.principal_id === permit.principal_id
|
||||
));
|
||||
return principalMatch && pathMatches(permit.resource_path, targetPath);
|
||||
});
|
||||
const denyMatch = matchingPermits.find((permit) => permit.effect === 'deny' && hasRequiredRights(permit.rights, requestedRights));
|
||||
if (denyMatch) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user