mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-16793] port credential generator service to providers (#14071)
* introduce extension service * deprecate legacy forwarder types * eliminate repeat algorithm emissions * extend logging to preference management * align forwarder ids with vendor ids * fix duplicate policy emissions; debugging required logger enhancements ----- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
This commit is contained in:
@@ -60,6 +60,7 @@ const SomeProvider = {
|
||||
} as LegacyEncryptorProvider,
|
||||
state: SomeStateProvider,
|
||||
log: disabledSemanticLoggerProvider,
|
||||
now: Date.now,
|
||||
} as UserStateSubjectDependencyProvider;
|
||||
|
||||
const SomeExtension: ExtensionMetadata = {
|
||||
|
||||
24
libs/common/src/tools/log/disabled-logger.ts
Normal file
24
libs/common/src/tools/log/disabled-logger.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { deepFreeze } from "../util";
|
||||
|
||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
/** All disabled loggers emitted by this module are `===` to this logger. */
|
||||
export const DISABLED_LOGGER: SemanticLogger = deepFreeze({
|
||||
debug<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||
|
||||
info<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||
|
||||
warn<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||
|
||||
error<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||
|
||||
panic<T>(content: Jsonify<T>, message?: string): never {
|
||||
if (typeof content === "string" && !message) {
|
||||
throw new Error(content);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
/** Disables semantic logs. Still panics. */
|
||||
export class DisabledSemanticLogger implements SemanticLogger {
|
||||
debug<T>(_content: Jsonify<T>, _message?: string): void {}
|
||||
|
||||
info<T>(_content: Jsonify<T>, _message?: string): void {}
|
||||
|
||||
warn<T>(_content: Jsonify<T>, _message?: string): void {}
|
||||
|
||||
error<T>(_content: Jsonify<T>, _message?: string): void {}
|
||||
|
||||
panic<T>(content: Jsonify<T>, message?: string): never {
|
||||
if (typeof content === "string" && !message) {
|
||||
throw new Error(content);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,10 @@ import { Jsonify } from "type-fest";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
|
||||
import { DefaultSemanticLogger } from "./default-semantic-logger";
|
||||
import { DisabledSemanticLogger } from "./disabled-semantic-logger";
|
||||
import { DISABLED_LOGGER } from "./disabled-logger";
|
||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
/** A type for injection of a log provider */
|
||||
export type LogProvider = <Context>(context: Jsonify<Context>) => SemanticLogger;
|
||||
import { LogProvider } from "./types";
|
||||
import { warnLoggingEnabled } from "./util";
|
||||
|
||||
/** Instantiates a semantic logger that emits nothing when a message
|
||||
* is logged.
|
||||
@@ -18,38 +17,72 @@ export type LogProvider = <Context>(context: Jsonify<Context>) => SemanticLogger
|
||||
export function disabledSemanticLoggerProvider<Context extends object>(
|
||||
_context: Jsonify<Context>,
|
||||
): SemanticLogger {
|
||||
return new DisabledSemanticLogger();
|
||||
return DISABLED_LOGGER;
|
||||
}
|
||||
|
||||
/** Instantiates a semantic logger that emits logs to the console.
|
||||
* @param context a static payload that is cloned when the logger
|
||||
* logs a message. The `messages`, `level`, and `content` fields
|
||||
* are reserved for use by loggers.
|
||||
* @param settings specializes how the semantic logger functions.
|
||||
* If this is omitted, the logger suppresses debug messages.
|
||||
* @param logService writes semantic logs to the console
|
||||
*/
|
||||
export function consoleSemanticLoggerProvider<Context extends object>(
|
||||
logger: LogService,
|
||||
context: Jsonify<Context>,
|
||||
): SemanticLogger {
|
||||
return new DefaultSemanticLogger(logger, context);
|
||||
export function consoleSemanticLoggerProvider(logService: LogService): LogProvider {
|
||||
function provider<Context extends object>(context: Jsonify<Context>) {
|
||||
const logger = new DefaultSemanticLogger(logService, context);
|
||||
|
||||
warnLoggingEnabled(logService, "consoleSemanticLoggerProvider", context);
|
||||
return logger;
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/** Instantiates a semantic logger that emits logs to the console.
|
||||
/** Instantiates a semantic logger that emits logs to the console when the
|
||||
* context's `type` matches its values.
|
||||
* @param logService writes semantic logs to the console
|
||||
* @param types the values to match against
|
||||
*/
|
||||
export function enableLogForTypes(logService: LogService, types: string[]): LogProvider {
|
||||
if (types.length) {
|
||||
warnLoggingEnabled(logService, "enableLogForTypes", { types });
|
||||
}
|
||||
|
||||
function provider<Context extends object>(context: Jsonify<Context>) {
|
||||
const { type } = context as { type?: unknown };
|
||||
if (typeof type === "string" && types.includes(type)) {
|
||||
const logger = new DefaultSemanticLogger(logService, context);
|
||||
|
||||
warnLoggingEnabled(logService, "enableLogForTypes", {
|
||||
targetType: type,
|
||||
available: types,
|
||||
loggerContext: context,
|
||||
});
|
||||
return logger;
|
||||
} else {
|
||||
return DISABLED_LOGGER;
|
||||
}
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/** Instantiates a semantic logger that emits logs to the console when its enabled.
|
||||
* @param enable logs are emitted when this is true
|
||||
* @param logService writes semantic logs to the console
|
||||
* @param context a static payload that is cloned when the logger
|
||||
* logs a message. The `messages`, `level`, and `content` fields
|
||||
* are reserved for use by loggers.
|
||||
* @param settings specializes how the semantic logger functions.
|
||||
* If this is omitted, the logger suppresses debug messages.
|
||||
* logs a message.
|
||||
*
|
||||
* @remarks The `message`, `level`, `provider`, and `content` fields
|
||||
* are reserved for use by the semantic logging system.
|
||||
*/
|
||||
export function ifEnabledSemanticLoggerProvider<Context extends object>(
|
||||
enable: boolean,
|
||||
logger: LogService,
|
||||
logService: LogService,
|
||||
context: Jsonify<Context>,
|
||||
) {
|
||||
if (enable) {
|
||||
return consoleSemanticLoggerProvider(logger, context);
|
||||
const logger = new DefaultSemanticLogger(logService, context);
|
||||
|
||||
warnLoggingEnabled(logService, "ifEnabledSemanticLoggerProvider", context);
|
||||
return logger;
|
||||
} else {
|
||||
return disabledSemanticLoggerProvider(context);
|
||||
return DISABLED_LOGGER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./factory";
|
||||
export * from "./disabled-logger";
|
||||
export { LogProvider } from "./types";
|
||||
export { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
11
libs/common/src/tools/log/types.ts
Normal file
11
libs/common/src/tools/log/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||
|
||||
/** Creates a semantic logger.
|
||||
* @param context all logs emitted by the logger are extended with
|
||||
* these fields.
|
||||
* @remarks The `message`, `level`, `provider`, and `content` fields
|
||||
* are reserved for use by the semantic logging system.
|
||||
*/
|
||||
export type LogProvider = <Context extends object>(context: Jsonify<Context>) => SemanticLogger;
|
||||
12
libs/common/src/tools/log/util.ts
Normal file
12
libs/common/src/tools/log/util.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
|
||||
// show our GRIT - these functions implement generalized logging
|
||||
// controls and should return DISABLED_LOGGER in production.
|
||||
export function warnLoggingEnabled(logService: LogService, method: string, context?: any) {
|
||||
logService.warning({
|
||||
method,
|
||||
context,
|
||||
provider: "tools/log",
|
||||
message: "Semantic logging enabled. 🦟 Please report this bug if you see it 🦟",
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export class PrivateClassifier<Data> implements Classifier<Data, Record<string,
|
||||
}
|
||||
const secret = picked as Jsonify<Data>;
|
||||
|
||||
return { disclosed: {}, secret };
|
||||
return { disclosed: null, secret };
|
||||
}
|
||||
|
||||
declassify(_disclosed: Jsonify<Record<keyof Data, never>>, secret: Jsonify<Data>) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class PublicClassifier<Data> implements Classifier<Data, Data, Record<str
|
||||
}
|
||||
const disclosed = picked as Jsonify<Data>;
|
||||
|
||||
return { disclosed, secret: "" };
|
||||
return { disclosed, secret: null };
|
||||
}
|
||||
|
||||
declassify(disclosed: Jsonify<Data>, _secret: Jsonify<Record<keyof Data, never>>) {
|
||||
|
||||
13
libs/common/src/tools/rx.rxjs.ts
Normal file
13
libs/common/src/tools/rx.rxjs.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
/**
|
||||
* Used to infer types from arguments to functions like {@link withLatestReady}.
|
||||
* So that you can have `forkJoin([Observable<A>, PromiseLike<B>]): Observable<[A, B]>`
|
||||
* et al.
|
||||
* @remarks this type definition is derived from rxjs' {@link ObservableInputTuple}.
|
||||
* The difference is it *only* works with observables, while the rx version works
|
||||
* with any thing that can become an observable.
|
||||
*/
|
||||
export type ObservableTuple<T> = {
|
||||
[K in keyof T]: Observable<T[K]>;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,13 @@ import {
|
||||
startWith,
|
||||
pairwise,
|
||||
MonoTypeOperatorFunction,
|
||||
Cons,
|
||||
scan,
|
||||
filter,
|
||||
} from "rxjs";
|
||||
|
||||
import { ObservableTuple } from "./rx.rxjs";
|
||||
|
||||
/** Returns its input. */
|
||||
function identity(value: any): any {
|
||||
return value;
|
||||
@@ -164,26 +169,30 @@ export function ready<T>(watch$: Observable<any> | Observable<any>[]) {
|
||||
);
|
||||
}
|
||||
|
||||
export function withLatestReady<Source, Watch>(
|
||||
watch$: Observable<Watch>,
|
||||
): OperatorFunction<Source, [Source, Watch]> {
|
||||
export function withLatestReady<Source, Watch extends readonly unknown[]>(
|
||||
...watches$: [...ObservableTuple<Watch>]
|
||||
): OperatorFunction<Source, Cons<Source, Watch>> {
|
||||
return connect((source$) => {
|
||||
// these subscriptions are safe because `source$` connects only after there
|
||||
// is an external subscriber.
|
||||
const source = new ReplaySubject<Source>(1);
|
||||
source$.subscribe(source);
|
||||
const watch = new ReplaySubject<Watch>(1);
|
||||
watch$.subscribe(watch);
|
||||
|
||||
const watches = watches$.map((w) => {
|
||||
const watch$ = new ReplaySubject<unknown>(1);
|
||||
w.subscribe(watch$);
|
||||
return watch$;
|
||||
}) as [...ObservableTuple<Watch>];
|
||||
|
||||
// `concat` is subscribed immediately after it's returned, at which point
|
||||
// `zip` blocks until all items in `watching$` are ready. If that occurs
|
||||
// `zip` blocks until all items in `watches` are ready. If that occurs
|
||||
// after `source$` is hot, then the replay subject sends the last-captured
|
||||
// emission through immediately. Otherwise, `ready` waits for the next
|
||||
// emission
|
||||
return concat(zip(watch).pipe(first(), ignoreElements()), source).pipe(
|
||||
withLatestFrom(watch),
|
||||
// emission through immediately. Otherwise, `withLatestFrom` waits for the
|
||||
// next emission
|
||||
return concat(zip(watches).pipe(first(), ignoreElements()), source).pipe(
|
||||
withLatestFrom(...watches),
|
||||
takeUntil(anyComplete(source)),
|
||||
);
|
||||
) as Observable<Cons<Source, Watch>>;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,3 +247,54 @@ export function pin<T>(options?: {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** maps a value to a result and keeps a cache of the mapping
|
||||
* @param mapResult - maps the stream to a result; this function must return
|
||||
* a value. It must not return null or undefined.
|
||||
* @param options.size - the number of entries in the cache
|
||||
* @param options.key - maps the source to a cache key
|
||||
* @remarks this method is useful for optimization of expensive
|
||||
* `mapResult` calls. It's also useful when an interned reference type
|
||||
* is needed.
|
||||
*/
|
||||
export function memoizedMap<Source, Result extends NonNullable<any>>(
|
||||
mapResult: (source: Source) => Result,
|
||||
options?: { size?: number; key?: (source: Source) => unknown },
|
||||
): OperatorFunction<Source, Result> {
|
||||
return pipe(
|
||||
// scan's accumulator contains the cache
|
||||
scan(
|
||||
([cache], source) => {
|
||||
const key: unknown = options?.key?.(source) ?? source;
|
||||
|
||||
// cache hit?
|
||||
let result = cache?.get(key);
|
||||
if (result) {
|
||||
return [cache, result] as const;
|
||||
}
|
||||
|
||||
// cache miss
|
||||
result = mapResult(source);
|
||||
cache?.set(key, result);
|
||||
|
||||
// trim cache
|
||||
const overage = cache.size - (options?.size ?? 1);
|
||||
if (overage > 0) {
|
||||
Array.from(cache?.keys() ?? [])
|
||||
.slice(0, overage)
|
||||
.forEach((k) => cache?.delete(k));
|
||||
}
|
||||
|
||||
return [cache, result] as const;
|
||||
},
|
||||
// FIXME: upgrade to a least-recently-used cache
|
||||
[new Map(), null] as [Map<unknown, Result>, Source | null],
|
||||
),
|
||||
|
||||
// encapsulate cache
|
||||
map(([, result]) => result),
|
||||
|
||||
// preserve `NonNullable` constraint on `Result`
|
||||
filter((result): result is Result => !!result),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,4 +15,9 @@ export abstract class UserStateSubjectDependencyProvider {
|
||||
// FIXME: remove `log` and inject the system provider into the USS instead
|
||||
/** Provides semantic logging */
|
||||
abstract log: <Context extends object>(_context: Jsonify<Context>) => SemanticLogger;
|
||||
|
||||
/** Get the system time as a number of seconds since the unix epoch
|
||||
* @remarks this can be turned into a date using `new Date(provider.now())`
|
||||
*/
|
||||
abstract now: () => number;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ const SomeProvider = {
|
||||
} as LegacyEncryptorProvider,
|
||||
state: SomeStateProvider,
|
||||
log: disabledSemanticLoggerProvider,
|
||||
now: () => 100,
|
||||
};
|
||||
|
||||
function fooMaxLength(maxLength: number): StateConstraints<TestType> {
|
||||
|
||||
@@ -477,7 +477,12 @@ export class UserStateSubject<
|
||||
* @returns the subscription
|
||||
*/
|
||||
subscribe(observer?: Partial<Observer<State>> | ((value: State) => void) | null): Subscription {
|
||||
return this.output.pipe(map((wc) => wc.state)).subscribe(observer);
|
||||
return this.output
|
||||
.pipe(
|
||||
map((wc) => wc.state),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
.subscribe(observer);
|
||||
}
|
||||
|
||||
// using subjects to ensure the right semantics are followed;
|
||||
|
||||
@@ -2,6 +2,11 @@ import { Simplify } from "type-fest";
|
||||
|
||||
import { IntegrationId } from "./integration";
|
||||
|
||||
/** When this is a string, it contains the i18n key. When it is an object, the `literal` member
|
||||
* contains text that should not be translated.
|
||||
*/
|
||||
export type I18nKeyOrLiteral = string | { literal: string };
|
||||
|
||||
/** Constraints that are shared by all primitive field types */
|
||||
type PrimitiveConstraint = {
|
||||
/** `true` indicates the field is required; otherwise the field is optional */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { I18nKeyOrLiteral } from "./types";
|
||||
|
||||
/** Recursively freeze an object's own keys
|
||||
* @param value the value to freeze
|
||||
* @returns `value`
|
||||
@@ -10,10 +12,22 @@ export function deepFreeze<T extends object>(value: T): Readonly<T> {
|
||||
for (const key of keys) {
|
||||
const own = value[key];
|
||||
|
||||
if ((own && typeof own === "object") || typeof own === "function") {
|
||||
if (own && typeof own === "object") {
|
||||
deepFreeze(own);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze(value);
|
||||
}
|
||||
|
||||
/** Type guard that returns `true` when the value is an i18n key. */
|
||||
export function isI18nKey(value: I18nKeyOrLiteral): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
/** Type guard that returns `true` when the value requires no translation.
|
||||
* @remarks the literal value can be accessed using the `.literal` property.
|
||||
*/
|
||||
export function isLiteral(value: I18nKeyOrLiteral): value is { literal: string } {
|
||||
return typeof value === "object" && "literal" in value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user