mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 09:58:46 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import type { SecurityPolicyFence, SecuritySubject } from '../security';
|
||||
import type { SecurityAuditRecord, SecurityAuditSink } from './securityAudit';
|
||||
import type {
|
||||
SecurityAuditQuery,
|
||||
SecurityAuditQueryPage,
|
||||
} from './securityAuditQuery';
|
||||
|
||||
export const MAX_LOCAL_SECURITY_AUDIT_QUERY_PAGE_SIZE = 64;
|
||||
|
||||
export interface LocalSecurityAuditQueryAuthorization {
|
||||
readonly authorityProjectId: string;
|
||||
readonly actor: SecuritySubject;
|
||||
readonly fence: SecurityPolicyFence;
|
||||
}
|
||||
|
||||
export interface ListAuthorizedLocalSecurityAuditCommand {
|
||||
readonly query: SecurityAuditQuery;
|
||||
readonly authorization: LocalSecurityAuditQueryAuthorization;
|
||||
readonly audit: SecurityAuditRecord;
|
||||
}
|
||||
|
||||
export interface ListAuthorizedLocalSecurityAuditResult
|
||||
extends SecurityAuditQueryPage {
|
||||
readonly audit: Readonly<SecurityAuditRecord>;
|
||||
}
|
||||
|
||||
export interface LocalSecurityAuditQueryRepository extends SecurityAuditSink {
|
||||
listAuthorized(
|
||||
command: ListAuthorizedLocalSecurityAuditCommand,
|
||||
): Promise<ListAuthorizedLocalSecurityAuditResult>;
|
||||
}
|
||||
|
||||
export class LocalSecurityAuditQueryAuthorizationFenceConflictError extends Error {
|
||||
readonly code = 'LOCAL_SECURITY_AUDIT_QUERY_AUTHORIZATION_FENCE_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Local security audit query authorization fence changed');
|
||||
this.name = 'LocalSecurityAuditQueryAuthorizationFenceConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecurityAuditQueryUnavailableError extends Error {
|
||||
readonly code = 'LOCAL_SECURITY_AUDIT_QUERY_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Local security audit query is unavailable');
|
||||
this.name = 'LocalSecurityAuditQueryUnavailableError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type { SecurityPolicyFence, SecuritySubject } from '../security';
|
||||
import {
|
||||
normalizeSecurityAuditRecord,
|
||||
type SecurityAuditRecord,
|
||||
type SecurityAuditSink,
|
||||
} from './securityAudit';
|
||||
import type { SecurityAuditQueryCursor } from './securityAuditQuery';
|
||||
|
||||
export const MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS = 30 * 24 * 60 * 60 * 1_000;
|
||||
export const MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS =
|
||||
10 * 365 * 24 * 60 * 60 * 1_000;
|
||||
export const MAX_EDGE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE = 64;
|
||||
export const MAX_STANDALONE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE = 512;
|
||||
export const MAX_LOCAL_SECURITY_AUDIT_COMPACTION_PAYLOAD_BYTES =
|
||||
16 * 1024 * 1024;
|
||||
|
||||
export interface LocalSecurityAuditRetentionAuthorization {
|
||||
readonly authorityProjectId: string;
|
||||
readonly actor: SecuritySubject;
|
||||
readonly fence: SecurityPolicyFence;
|
||||
}
|
||||
|
||||
export interface LocalSecurityAuditCompactionRecord {
|
||||
readonly mutationId: string;
|
||||
readonly requestId: string;
|
||||
readonly authorityProjectId: string;
|
||||
readonly retentionMs: number;
|
||||
readonly eligibleBeforeMs: number;
|
||||
readonly batchLimit: number;
|
||||
readonly deletedCount: number;
|
||||
readonly deletedPayloadBytes: number;
|
||||
readonly first: Readonly<SecurityAuditQueryCursor> | null;
|
||||
readonly last: Readonly<SecurityAuditQueryCursor> | null;
|
||||
readonly recordsDigest: string;
|
||||
readonly createdAtMs: number;
|
||||
}
|
||||
|
||||
export interface CompactAuthorizedLocalSecurityAuditCommand {
|
||||
readonly mutationId: string;
|
||||
readonly requestId: string;
|
||||
readonly retentionMs: number;
|
||||
readonly eligibleBeforeMs: number;
|
||||
readonly limit: number;
|
||||
readonly authorization: LocalSecurityAuditRetentionAuthorization;
|
||||
readonly audit: SecurityAuditRecord;
|
||||
}
|
||||
|
||||
export interface CompactAuthorizedLocalSecurityAuditResult {
|
||||
readonly status: 'inserted' | 'existing';
|
||||
readonly record: Readonly<LocalSecurityAuditCompactionRecord>;
|
||||
readonly audit: Readonly<SecurityAuditRecord>;
|
||||
}
|
||||
|
||||
export interface LocalSecurityAuditRetentionRepository
|
||||
extends SecurityAuditSink {
|
||||
resolveCompaction(
|
||||
mutationId: string,
|
||||
): Promise<Readonly<LocalSecurityAuditCompactionRecord> | null>;
|
||||
compactAuthorized(
|
||||
command: CompactAuthorizedLocalSecurityAuditCommand,
|
||||
): Promise<CompactAuthorizedLocalSecurityAuditResult>;
|
||||
}
|
||||
|
||||
export class InvalidLocalSecurityAuditRetentionValueError extends TypeError {
|
||||
constructor(message: string) {
|
||||
super(`Local security audit retention value is invalid: ${message}`);
|
||||
this.name = 'InvalidLocalSecurityAuditRetentionValueError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecurityAuditRetentionAuthorizationFenceConflictError extends Error {
|
||||
readonly code = 'LOCAL_SECURITY_AUDIT_RETENTION_AUTHORIZATION_FENCE_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Local security audit retention authorization fence changed');
|
||||
this.name = 'LocalSecurityAuditRetentionAuthorizationFenceConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecurityAuditCompactionMutationConflictError extends Error {
|
||||
readonly code = 'LOCAL_SECURITY_AUDIT_COMPACTION_MUTATION_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Local security audit compaction mutation conflicts');
|
||||
this.name = 'LocalSecurityAuditCompactionMutationConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSecurityAuditRetentionUnavailableError extends Error {
|
||||
readonly code = 'LOCAL_SECURITY_AUDIT_RETENTION_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Local security audit retention is unavailable');
|
||||
this.name = 'LocalSecurityAuditRetentionUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
export function localSecurityAuditCompactionPayload(
|
||||
records: readonly Readonly<SecurityAuditRecord>[],
|
||||
): Readonly<{ recordsDigest: string; payloadBytes: number }> {
|
||||
if (!Array.isArray(records)) {
|
||||
throw new InvalidLocalSecurityAuditRetentionValueError(
|
||||
'records must be an array',
|
||||
);
|
||||
}
|
||||
let normalized: Readonly<SecurityAuditRecord>[];
|
||||
try {
|
||||
normalized = records.map((record) => normalizeSecurityAuditRecord(record));
|
||||
} catch {
|
||||
throw new InvalidLocalSecurityAuditRetentionValueError(
|
||||
'records contain an invalid audit value',
|
||||
);
|
||||
}
|
||||
const payload = JSON.stringify(normalized);
|
||||
const payloadBytes =
|
||||
normalized.length === 0 ? 0 : Buffer.byteLength(payload, 'utf8');
|
||||
if (payloadBytes > MAX_LOCAL_SECURITY_AUDIT_COMPACTION_PAYLOAD_BYTES) {
|
||||
throw new InvalidLocalSecurityAuditRetentionValueError(
|
||||
'records exceed the payload limit',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
recordsDigest: createHash('sha256')
|
||||
.update('qinglong.local-security-audit-compaction.records.v1\0', 'utf8')
|
||||
.update(payload, 'utf8')
|
||||
.digest('hex'),
|
||||
payloadBytes,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import type { SecurityPolicyFence, SecuritySubject } from '../security';
|
||||
import { SECURITY_SUBJECT_TYPES } from '../security';
|
||||
|
||||
export const SECURITY_AUDIT_OUTCOMES = [
|
||||
'authentication_rejected',
|
||||
'authentication_unavailable',
|
||||
'authorization_unavailable',
|
||||
'denied',
|
||||
'approval_required',
|
||||
'allowed',
|
||||
] as const;
|
||||
|
||||
export type SecurityAuditOutcome = (typeof SECURITY_AUDIT_OUTCOMES)[number];
|
||||
|
||||
export interface SecurityAuditRecord {
|
||||
readonly eventId: string;
|
||||
readonly requestId: string;
|
||||
readonly operationId: string;
|
||||
readonly projectId: string | null;
|
||||
readonly subject: SecuritySubject | null;
|
||||
readonly authenticationId: string | null;
|
||||
readonly outcome: SecurityAuditOutcome;
|
||||
readonly reasons: readonly string[];
|
||||
readonly fence: SecurityPolicyFence | null;
|
||||
readonly occurredAtMs: number;
|
||||
}
|
||||
|
||||
export interface SecurityAuditSink {
|
||||
record(record: SecurityAuditRecord): void | Promise<void>;
|
||||
}
|
||||
|
||||
export class InvalidSecurityAuditValueError extends TypeError {
|
||||
constructor(message: string) {
|
||||
super(`Security audit value is invalid: ${message}`);
|
||||
this.name = 'InvalidSecurityAuditValueError';
|
||||
}
|
||||
}
|
||||
|
||||
export class SecurityAuditUnavailableError extends Error {
|
||||
readonly code = 'SECURITY_AUDIT_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Security audit storage is unavailable');
|
||||
this.name = 'SecurityAuditUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
const UUID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const OPERATION_PATTERN = /^[a-z][a-z0-9_.:-]{0,127}$/;
|
||||
const PROJECT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const AUTHENTICATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const REASON_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
||||
const SUBJECT_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
function exactKeys(
|
||||
value: object,
|
||||
expected: readonly string[],
|
||||
name: string,
|
||||
): void {
|
||||
const keys = Object.keys(value).sort();
|
||||
const canonical = [...expected].sort();
|
||||
if (
|
||||
keys.length !== canonical.length ||
|
||||
keys.some((key, index) => key !== canonical[index])
|
||||
) {
|
||||
throw new InvalidSecurityAuditValueError(`${name} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSubject(
|
||||
value: SecuritySubject | null,
|
||||
): Readonly<SecuritySubject> | null {
|
||||
if (value === null) return null;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new InvalidSecurityAuditValueError('subject is invalid');
|
||||
}
|
||||
exactKeys(value, ['type', 'id'], 'subject');
|
||||
if (
|
||||
!SECURITY_SUBJECT_TYPES.includes(value.type) ||
|
||||
typeof value.id !== 'string' ||
|
||||
value.id.length < 1 ||
|
||||
value.id.length > 255 ||
|
||||
SUBJECT_ID_CONTROL_PATTERN.test(value.id)
|
||||
) {
|
||||
throw new InvalidSecurityAuditValueError('subject is invalid');
|
||||
}
|
||||
return Object.freeze({ type: value.type, id: value.id });
|
||||
}
|
||||
|
||||
function normalizeFence(
|
||||
value: SecurityPolicyFence | null,
|
||||
): Readonly<SecurityPolicyFence> | null {
|
||||
if (value === null) return null;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new InvalidSecurityAuditValueError('fence is invalid');
|
||||
}
|
||||
exactKeys(value, ['projectVersion', 'bindingVersion'], 'fence');
|
||||
if (
|
||||
!Number.isSafeInteger(value.projectVersion) ||
|
||||
value.projectVersion < 1 ||
|
||||
(value.bindingVersion !== null &&
|
||||
(!Number.isSafeInteger(value.bindingVersion) || value.bindingVersion < 1))
|
||||
) {
|
||||
throw new InvalidSecurityAuditValueError('fence is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
projectVersion: value.projectVersion,
|
||||
bindingVersion: value.bindingVersion,
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeSecurityAuditRecord(
|
||||
value: SecurityAuditRecord,
|
||||
): Readonly<SecurityAuditRecord> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new InvalidSecurityAuditValueError('record must be an object');
|
||||
}
|
||||
exactKeys(
|
||||
value,
|
||||
[
|
||||
'eventId',
|
||||
'requestId',
|
||||
'operationId',
|
||||
'projectId',
|
||||
'subject',
|
||||
'authenticationId',
|
||||
'outcome',
|
||||
'reasons',
|
||||
'fence',
|
||||
'occurredAtMs',
|
||||
],
|
||||
'record',
|
||||
);
|
||||
if (!UUID_PATTERN.test(value.eventId)) {
|
||||
throw new InvalidSecurityAuditValueError('eventId is invalid');
|
||||
}
|
||||
if (!REQUEST_ID_PATTERN.test(value.requestId)) {
|
||||
throw new InvalidSecurityAuditValueError('requestId is invalid');
|
||||
}
|
||||
if (!OPERATION_PATTERN.test(value.operationId)) {
|
||||
throw new InvalidSecurityAuditValueError('operationId is invalid');
|
||||
}
|
||||
if (value.projectId !== null && !PROJECT_PATTERN.test(value.projectId)) {
|
||||
throw new InvalidSecurityAuditValueError('projectId is invalid');
|
||||
}
|
||||
const subject = normalizeSubject(value.subject);
|
||||
if (
|
||||
(value.authenticationId === null) !== (subject === null) ||
|
||||
(value.authenticationId !== null &&
|
||||
!AUTHENTICATION_ID_PATTERN.test(value.authenticationId))
|
||||
) {
|
||||
throw new InvalidSecurityAuditValueError('authenticationId is invalid');
|
||||
}
|
||||
if (!SECURITY_AUDIT_OUTCOMES.includes(value.outcome)) {
|
||||
throw new InvalidSecurityAuditValueError('outcome is invalid');
|
||||
}
|
||||
const preAuthentication =
|
||||
value.outcome === 'authentication_rejected' ||
|
||||
value.outcome === 'authentication_unavailable';
|
||||
if (preAuthentication !== (subject === null)) {
|
||||
throw new InvalidSecurityAuditValueError('outcome identity is invalid');
|
||||
}
|
||||
if (
|
||||
!Array.isArray(value.reasons) ||
|
||||
value.reasons.length < 1 ||
|
||||
value.reasons.length > 8 ||
|
||||
value.reasons.some(
|
||||
(reason) => typeof reason !== 'string' || !REASON_PATTERN.test(reason),
|
||||
)
|
||||
) {
|
||||
throw new InvalidSecurityAuditValueError('reasons are invalid');
|
||||
}
|
||||
const fence = normalizeFence(value.fence);
|
||||
if (!Number.isSafeInteger(value.occurredAtMs) || value.occurredAtMs < 0) {
|
||||
throw new InvalidSecurityAuditValueError('occurredAtMs is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
eventId: value.eventId,
|
||||
requestId: value.requestId,
|
||||
operationId: value.operationId,
|
||||
projectId: value.projectId,
|
||||
subject,
|
||||
authenticationId: value.authenticationId,
|
||||
outcome: value.outcome,
|
||||
reasons: Object.freeze([...value.reasons]),
|
||||
fence,
|
||||
occurredAtMs: value.occurredAtMs,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
SECURITY_AUDIT_OUTCOMES,
|
||||
type SecurityAuditOutcome,
|
||||
type SecurityAuditRecord,
|
||||
} from './securityAudit';
|
||||
import { SECURITY_SUBJECT_TYPES, type SecuritySubject } from '../security';
|
||||
|
||||
export const MAX_SECURITY_AUDIT_QUERY_PAGE_SIZE = 200;
|
||||
|
||||
export interface SecurityAuditQueryCursor {
|
||||
readonly occurredAtMs: number;
|
||||
readonly eventId: string;
|
||||
}
|
||||
|
||||
export interface SecurityAuditQueryFilter {
|
||||
readonly projectId?: string;
|
||||
readonly subject?: SecuritySubject;
|
||||
readonly outcome?: SecurityAuditOutcome;
|
||||
}
|
||||
|
||||
export interface SecurityAuditQuery {
|
||||
readonly limit: number;
|
||||
readonly before?: SecurityAuditQueryCursor;
|
||||
readonly filter: SecurityAuditQueryFilter;
|
||||
}
|
||||
|
||||
export interface SecurityAuditQueryPage {
|
||||
readonly records: readonly Readonly<SecurityAuditRecord>[];
|
||||
readonly nextCursor: Readonly<SecurityAuditQueryCursor> | null;
|
||||
}
|
||||
|
||||
export interface SecurityAuditQueryRepository {
|
||||
list(query: SecurityAuditQuery): Promise<SecurityAuditQueryPage>;
|
||||
}
|
||||
|
||||
export class InvalidSecurityAuditQueryError extends TypeError {
|
||||
constructor(message: string) {
|
||||
super(`Security audit query is invalid: ${message}`);
|
||||
this.name = 'InvalidSecurityAuditQueryError';
|
||||
}
|
||||
}
|
||||
|
||||
export class SecurityAuditQueryUnavailableError extends Error {
|
||||
readonly code = 'SECURITY_AUDIT_QUERY_UNAVAILABLE';
|
||||
|
||||
constructor() {
|
||||
super('Security audit query is unavailable');
|
||||
this.name = 'SecurityAuditQueryUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
const UUID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const PROJECT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const SUBJECT_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
||||
|
||||
function exactKeys(
|
||||
value: object,
|
||||
expected: readonly string[],
|
||||
name: string,
|
||||
): void {
|
||||
const keys = Object.keys(value).sort();
|
||||
const canonical = [...expected].sort();
|
||||
if (
|
||||
keys.length !== canonical.length ||
|
||||
keys.some((key, index) => key !== canonical[index])
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError(`${name} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSecurityAuditQuery(
|
||||
value: SecurityAuditQuery,
|
||||
): Readonly<SecurityAuditQuery> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new InvalidSecurityAuditQueryError('query must be an object');
|
||||
}
|
||||
const topKeys = ['limit', 'filter'];
|
||||
if (value.before !== undefined) topKeys.push('before');
|
||||
exactKeys(value, topKeys, 'query');
|
||||
if (
|
||||
!Number.isSafeInteger(value.limit) ||
|
||||
value.limit < 1 ||
|
||||
value.limit > MAX_SECURITY_AUDIT_QUERY_PAGE_SIZE
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('limit is invalid');
|
||||
}
|
||||
let before: Readonly<SecurityAuditQueryCursor> | undefined;
|
||||
if (value.before !== undefined) {
|
||||
if (
|
||||
!value.before ||
|
||||
typeof value.before !== 'object' ||
|
||||
Array.isArray(value.before)
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('before is invalid');
|
||||
}
|
||||
exactKeys(value.before, ['occurredAtMs', 'eventId'], 'before');
|
||||
if (
|
||||
!Number.isSafeInteger(value.before.occurredAtMs) ||
|
||||
value.before.occurredAtMs < 0 ||
|
||||
!UUID_PATTERN.test(value.before.eventId)
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('before is invalid');
|
||||
}
|
||||
before = Object.freeze({ ...value.before });
|
||||
}
|
||||
if (
|
||||
!value.filter ||
|
||||
typeof value.filter !== 'object' ||
|
||||
Array.isArray(value.filter)
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('filter is invalid');
|
||||
}
|
||||
const filterKeys = Object.keys(value.filter);
|
||||
if (
|
||||
filterKeys.some((key) => !['projectId', 'subject', 'outcome'].includes(key))
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('filter shape is invalid');
|
||||
}
|
||||
const filter: {
|
||||
projectId?: string;
|
||||
subject?: Readonly<SecuritySubject>;
|
||||
outcome?: SecurityAuditOutcome;
|
||||
} = {};
|
||||
if (value.filter.projectId !== undefined) {
|
||||
if (!PROJECT_PATTERN.test(value.filter.projectId)) {
|
||||
throw new InvalidSecurityAuditQueryError('projectId is invalid');
|
||||
}
|
||||
filter.projectId = value.filter.projectId;
|
||||
}
|
||||
if (value.filter.subject !== undefined) {
|
||||
const candidate = value.filter.subject;
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
Array.isArray(candidate)
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('subject is invalid');
|
||||
}
|
||||
exactKeys(candidate, ['type', 'id'], 'subject');
|
||||
if (
|
||||
!SECURITY_SUBJECT_TYPES.includes(candidate.type) ||
|
||||
typeof candidate.id !== 'string' ||
|
||||
candidate.id.length < 1 ||
|
||||
candidate.id.length > 255 ||
|
||||
SUBJECT_ID_CONTROL_PATTERN.test(candidate.id)
|
||||
) {
|
||||
throw new InvalidSecurityAuditQueryError('subject is invalid');
|
||||
}
|
||||
filter.subject = Object.freeze({ type: candidate.type, id: candidate.id });
|
||||
}
|
||||
if (value.filter.outcome !== undefined) {
|
||||
if (!SECURITY_AUDIT_OUTCOMES.includes(value.filter.outcome)) {
|
||||
throw new InvalidSecurityAuditQueryError('outcome is invalid');
|
||||
}
|
||||
filter.outcome = value.filter.outcome;
|
||||
}
|
||||
return Object.freeze({
|
||||
limit: value.limit,
|
||||
...(before ? { before } : {}),
|
||||
filter: Object.freeze(filter),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user