@siteping/server 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/dist/concurrency.d.cts +14 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/deep-link.d.cts +14 -0
- package/dist/deep-link.d.ts +14 -0
- package/dist/email.d.cts +22 -0
- package/dist/email.d.ts +22 -0
- package/dist/errors.d.cts +55 -0
- package/dist/errors.d.ts +55 -0
- package/dist/filters.d.cts +82 -0
- package/dist/filters.d.ts +82 -0
- package/dist/i18n.d.cts +85 -0
- package/dist/i18n.d.ts +85 -0
- package/dist/index.cjs +1270 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +488 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +1237 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.cts +317 -0
- package/dist/schema.d.ts +317 -0
- package/dist/screenshot-storage.d.cts +103 -0
- package/dist/screenshot-storage.d.ts +103 -0
- package/dist/siteping-core.d.cts +20 -0
- package/dist/siteping-core.d.ts +20 -0
- package/dist/store-helpers.d.cts +140 -0
- package/dist/store-helpers.d.ts +140 -0
- package/dist/type-utils.d.cts +66 -0
- package/dist/type-utils.d.ts +66 -0
- package/dist/types.d.cts +1206 -0
- package/dist/types.d.ts +1206 -0
- package/dist/wire.d.cts +68 -0
- package/dist/wire.d.ts +68 -0
- package/package.json +64 -4
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable storage for feedback screenshots.
|
|
3
|
+
*
|
|
4
|
+
* `adapter-prisma` and `adapter-drizzle` accept an optional
|
|
5
|
+
* `screenshotStorage` config. When provided, the adapter forwards the
|
|
6
|
+
* widget-supplied data URL to `upload()` and persists the returned URL on
|
|
7
|
+
* `Feedback.screenshotUrl`. When not provided, the adapter falls back to
|
|
8
|
+
* inline base64 (with a one-time warn) — fine for dev and small
|
|
9
|
+
* deployments, a footgun for production Postgres.
|
|
10
|
+
*
|
|
11
|
+
* `@siteping/screenshot-storage` implements it over any S3-compatible
|
|
12
|
+
* bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO…), Cloudflare Images,
|
|
13
|
+
* a database table, the local filesystem or memory. Implement it yourself
|
|
14
|
+
* for anything else.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Minimal S3 implementation
|
|
19
|
+
* import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
20
|
+
*
|
|
21
|
+
* const s3 = new S3Client({ region: "eu-west-3" });
|
|
22
|
+
* const screenshotStorage: ScreenshotStorage = {
|
|
23
|
+
* async upload(dataUrl, { mimeType }) {
|
|
24
|
+
* const body = Buffer.from(dataUrl.slice(dataUrl.indexOf(",") + 1), "base64");
|
|
25
|
+
* const key = `siteping/${crypto.randomUUID()}`; // fresh per upload, see URL ownership
|
|
26
|
+
* await s3.send(new PutObjectCommand({
|
|
27
|
+
* Bucket: "my-bucket", Key: key, Body: body, ContentType: mimeType,
|
|
28
|
+
* }));
|
|
29
|
+
* return { url: `https://cdn.example.com/${key}` };
|
|
30
|
+
* },
|
|
31
|
+
* };
|
|
32
|
+
*
|
|
33
|
+
* createSitepingHandler({ prisma, screenshotStorage });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export interface ScreenshotStorage {
|
|
37
|
+
/**
|
|
38
|
+
* Persist a base64 data URL and return the URL the widget will use as
|
|
39
|
+
* `<img src>`. Implementations decide the underlying storage and any
|
|
40
|
+
* post-processing (resize, virus scan, content-type sniff).
|
|
41
|
+
*
|
|
42
|
+
* Adapters call this synchronously inside `createFeedback` — keep it
|
|
43
|
+
* fast or move to a queue if needed.
|
|
44
|
+
*
|
|
45
|
+
* `ctx.feedbackId` identifies the upload. Adapters that upload before the
|
|
46
|
+
* record exists pass the *client-generated* `clientId` (Prisma); the
|
|
47
|
+
* Drizzle adapter passes the server-generated id the create attempt will
|
|
48
|
+
* insert the record under.
|
|
49
|
+
*
|
|
50
|
+
* **URL ownership:** every call must return a URL no other call returns —
|
|
51
|
+
* key the object by a fresh random value (`crypto.randomUUID()`, as the
|
|
52
|
+
* example does), never by `ctx.feedbackId` alone nor by a content hash.
|
|
53
|
+
* Adapters treat each URL as the property of the one record that stores it
|
|
54
|
+
* and may pass it to `delete` once that record is deleted, or once its
|
|
55
|
+
* create lost a race: two submissions of one `clientId` both upload, and
|
|
56
|
+
* the object of the one that is not stored is deleted. Under Prisma,
|
|
57
|
+
* `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed
|
|
58
|
+
* by it is rewritten by every replay — a retry, or anyone who learns the
|
|
59
|
+
* id — and a URL shared by several records lets deleting one remove the
|
|
60
|
+
* object another still points at.
|
|
61
|
+
*
|
|
62
|
+
* **Security note:** treat `ctx.feedbackId` as attacker-controlled:
|
|
63
|
+
* sanitize before using it in filesystem paths or object keys, even though
|
|
64
|
+
* server adapters validate its shape upstream.
|
|
65
|
+
*/
|
|
66
|
+
upload(dataUrl: string, ctx: {
|
|
67
|
+
feedbackId: string;
|
|
68
|
+
mimeType: string;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
url: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional cleanup hook called when the feedback is deleted, and for an
|
|
74
|
+
* object uploaded by a create whose record was not stored: one rejected
|
|
75
|
+
* because its `clientId` is already stored, or — in adapters that key
|
|
76
|
+
* uploads per attempt, like Drizzle — one whose insert failed. An object a
|
|
77
|
+
* stored row still references (a deterministic key reused by the replay)
|
|
78
|
+
* is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
|
|
79
|
+
* Adapters call this best-effort and swallow errors — orphaned objects are
|
|
80
|
+
* preferred over failed deletes.
|
|
81
|
+
*/
|
|
82
|
+
delete?: (url: string) => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Most `ScreenshotStorage.delete` calls one cleanup keeps in flight — a
|
|
86
|
+
* project delete may free thousands of objects, and firing them all at once
|
|
87
|
+
* can exhaust sockets (one per request with `fetch`) or memory and trip
|
|
88
|
+
* object-store rate limits. It stays under the 50 sockets the AWS SDK opens
|
|
89
|
+
* per client by default, yet a project delete, which waits for its cleanup,
|
|
90
|
+
* is not serialised: 1,000 objects take 32 rounds. Adapters run their
|
|
91
|
+
* deletes through `settleWithConcurrencyLimit` with this bound.
|
|
92
|
+
*/
|
|
93
|
+
export declare const SCREENSHOT_DELETE_CONCURRENCY = 32;
|
|
94
|
+
/**
|
|
95
|
+
* MIME type an adapter reports to {@link ScreenshotStorage.upload}: the one
|
|
96
|
+
* an image data URL declares (`data:image/png;base64,…` → `image/png`),
|
|
97
|
+
* limited to the JPEG, PNG and WebP the HTTP schema accepts. Stores are
|
|
98
|
+
* public and may be fed unvalidated data URLs, and an `image/svg+xml` label
|
|
99
|
+
* would make the stored object script-capable when served inline. Anything
|
|
100
|
+
* else — including a data URL that declares no type — reports JPEG, the
|
|
101
|
+
* widget's capture format.
|
|
102
|
+
*/
|
|
103
|
+
export declare function screenshotMimeType(dataUrl: string): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable storage for feedback screenshots.
|
|
3
|
+
*
|
|
4
|
+
* `adapter-prisma` and `adapter-drizzle` accept an optional
|
|
5
|
+
* `screenshotStorage` config. When provided, the adapter forwards the
|
|
6
|
+
* widget-supplied data URL to `upload()` and persists the returned URL on
|
|
7
|
+
* `Feedback.screenshotUrl`. When not provided, the adapter falls back to
|
|
8
|
+
* inline base64 (with a one-time warn) — fine for dev and small
|
|
9
|
+
* deployments, a footgun for production Postgres.
|
|
10
|
+
*
|
|
11
|
+
* `@siteping/screenshot-storage` implements it over any S3-compatible
|
|
12
|
+
* bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO…), Cloudflare Images,
|
|
13
|
+
* a database table, the local filesystem or memory. Implement it yourself
|
|
14
|
+
* for anything else.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Minimal S3 implementation
|
|
19
|
+
* import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
20
|
+
*
|
|
21
|
+
* const s3 = new S3Client({ region: "eu-west-3" });
|
|
22
|
+
* const screenshotStorage: ScreenshotStorage = {
|
|
23
|
+
* async upload(dataUrl, { mimeType }) {
|
|
24
|
+
* const body = Buffer.from(dataUrl.slice(dataUrl.indexOf(",") + 1), "base64");
|
|
25
|
+
* const key = `siteping/${crypto.randomUUID()}`; // fresh per upload, see URL ownership
|
|
26
|
+
* await s3.send(new PutObjectCommand({
|
|
27
|
+
* Bucket: "my-bucket", Key: key, Body: body, ContentType: mimeType,
|
|
28
|
+
* }));
|
|
29
|
+
* return { url: `https://cdn.example.com/${key}` };
|
|
30
|
+
* },
|
|
31
|
+
* };
|
|
32
|
+
*
|
|
33
|
+
* createSitepingHandler({ prisma, screenshotStorage });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export interface ScreenshotStorage {
|
|
37
|
+
/**
|
|
38
|
+
* Persist a base64 data URL and return the URL the widget will use as
|
|
39
|
+
* `<img src>`. Implementations decide the underlying storage and any
|
|
40
|
+
* post-processing (resize, virus scan, content-type sniff).
|
|
41
|
+
*
|
|
42
|
+
* Adapters call this synchronously inside `createFeedback` — keep it
|
|
43
|
+
* fast or move to a queue if needed.
|
|
44
|
+
*
|
|
45
|
+
* `ctx.feedbackId` identifies the upload. Adapters that upload before the
|
|
46
|
+
* record exists pass the *client-generated* `clientId` (Prisma); the
|
|
47
|
+
* Drizzle adapter passes the server-generated id the create attempt will
|
|
48
|
+
* insert the record under.
|
|
49
|
+
*
|
|
50
|
+
* **URL ownership:** every call must return a URL no other call returns —
|
|
51
|
+
* key the object by a fresh random value (`crypto.randomUUID()`, as the
|
|
52
|
+
* example does), never by `ctx.feedbackId` alone nor by a content hash.
|
|
53
|
+
* Adapters treat each URL as the property of the one record that stores it
|
|
54
|
+
* and may pass it to `delete` once that record is deleted, or once its
|
|
55
|
+
* create lost a race: two submissions of one `clientId` both upload, and
|
|
56
|
+
* the object of the one that is not stored is deleted. Under Prisma,
|
|
57
|
+
* `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed
|
|
58
|
+
* by it is rewritten by every replay — a retry, or anyone who learns the
|
|
59
|
+
* id — and a URL shared by several records lets deleting one remove the
|
|
60
|
+
* object another still points at.
|
|
61
|
+
*
|
|
62
|
+
* **Security note:** treat `ctx.feedbackId` as attacker-controlled:
|
|
63
|
+
* sanitize before using it in filesystem paths or object keys, even though
|
|
64
|
+
* server adapters validate its shape upstream.
|
|
65
|
+
*/
|
|
66
|
+
upload(dataUrl: string, ctx: {
|
|
67
|
+
feedbackId: string;
|
|
68
|
+
mimeType: string;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
url: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional cleanup hook called when the feedback is deleted, and for an
|
|
74
|
+
* object uploaded by a create whose record was not stored: one rejected
|
|
75
|
+
* because its `clientId` is already stored, or — in adapters that key
|
|
76
|
+
* uploads per attempt, like Drizzle — one whose insert failed. An object a
|
|
77
|
+
* stored row still references (a deterministic key reused by the replay)
|
|
78
|
+
* is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
|
|
79
|
+
* Adapters call this best-effort and swallow errors — orphaned objects are
|
|
80
|
+
* preferred over failed deletes.
|
|
81
|
+
*/
|
|
82
|
+
delete?: (url: string) => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Most `ScreenshotStorage.delete` calls one cleanup keeps in flight — a
|
|
86
|
+
* project delete may free thousands of objects, and firing them all at once
|
|
87
|
+
* can exhaust sockets (one per request with `fetch`) or memory and trip
|
|
88
|
+
* object-store rate limits. It stays under the 50 sockets the AWS SDK opens
|
|
89
|
+
* per client by default, yet a project delete, which waits for its cleanup,
|
|
90
|
+
* is not serialised: 1,000 objects take 32 rounds. Adapters run their
|
|
91
|
+
* deletes through `settleWithConcurrencyLimit` with this bound.
|
|
92
|
+
*/
|
|
93
|
+
export declare const SCREENSHOT_DELETE_CONCURRENCY = 32;
|
|
94
|
+
/**
|
|
95
|
+
* MIME type an adapter reports to {@link ScreenshotStorage.upload}: the one
|
|
96
|
+
* an image data URL declares (`data:image/png;base64,…` → `image/png`),
|
|
97
|
+
* limited to the JPEG, PNG and WebP the HTTP schema accepts. Stores are
|
|
98
|
+
* public and may be fed unvalidated data URLs, and an `image/svg+xml` label
|
|
99
|
+
* would make the stored object script-capable when served inline. Anything
|
|
100
|
+
* else — including a data URL that declares no type — reports JPEG, the
|
|
101
|
+
* widget's capture format.
|
|
102
|
+
*/
|
|
103
|
+
export declare function screenshotMimeType(dataUrl: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { settleWithConcurrencyLimit } from "./concurrency.cjs";
|
|
2
|
+
export { buildDeepLink, parseHttpUrl } from "./deep-link.cjs";
|
|
3
|
+
export { EMAIL_PATTERN, isValidEmail } from "./email.cjs";
|
|
4
|
+
export type { SitepingErrorCode } from "./errors.cjs";
|
|
5
|
+
export { SitepingAuthError, SitepingError, SitepingNetworkError, SitepingValidationError } from "./errors.cjs";
|
|
6
|
+
export type { FilterResult, Pagination } from "./filters.cjs";
|
|
7
|
+
export { applyFeedbackFilters, clampPagination, DEFAULT_PAGE_LIMIT, isUnreachableOffset, MAX_PAGE_LIMIT, matchesFeedbackQuery, } from "./filters.cjs";
|
|
8
|
+
export type { I18n, LocaleLoaders, TranslateFunction } from "./i18n.cjs";
|
|
9
|
+
export { canonicalizeLocale, createI18n, interpolate, intlLocale, tWithParams } from "./i18n.cjs";
|
|
10
|
+
export type { FieldDef, IndexDef, ModelDef, PrismaNativeType, PrismaScalarType, RelationDef, RelationKind, RelationOnDelete, SitepingModelFieldName, SitepingModelName, } from "./schema.cjs";
|
|
11
|
+
export { isRelationField, isScalarField, SITEPING_MODELS } from "./schema.cjs";
|
|
12
|
+
export type { ScreenshotStorage } from "./screenshot-storage.cjs";
|
|
13
|
+
export { SCREENSHOT_DELETE_CONCURRENCY, screenshotMimeType } from "./screenshot-storage.cjs";
|
|
14
|
+
export type { CollectionStore, CollectionStoreBackend } from "./store-helpers.cjs";
|
|
15
|
+
export { buildAnnotationRecord, buildCommentRecord, buildFeedbackRecord, createCollectionStore, } from "./store-helpers.cjs";
|
|
16
|
+
export type { AssertEqual, IfEquals, Prettify, Serialized } from "./type-utils.cjs";
|
|
17
|
+
export { hasOwn, isRecord } from "./type-utils.cjs";
|
|
18
|
+
export type { AnchorData, AnnotationCreateInput, AnnotationPayload, AnnotationRecord, AnnotationResponse, BuiltinLocale, ClosedFeedbackStatus, CommentAuthorRole, CommentCreateInput, CommentDeletePayload, CommentPayload, CommentRecord, CommentResponse, ConsoleDiagnosticEntry, ConsoleDiagnosticLevel, DiagnosticsCaptureOptions, DiagnosticsSnapshot, FeedbackCreateInput, FeedbackCreateOutcome, FeedbackListPermissions, FeedbackPage, FeedbackPayload, FeedbackPermissions, FeedbackQuery, FeedbackRecord, FeedbackResponse, FeedbackResponseList, FeedbackStatus, FeedbackType, FeedbackUpdateInput, NetworkDiagnosticEntry, OpenFeedbackStatus, PageScope, RectData, ScreenshotRegion, SitepingBaseConfig, SitepingCapabilities, SitepingConfig, SitepingDeepLinkOptions, SitepingHeadersOption, SitepingHttpConfig, SitepingIdentity, SitepingInstance, SitepingLocale, SitepingPanelAction, SitepingPanelActionContext, SitepingPanelActionFeedback, SitepingPanelButtonAction, SitepingPanelLinkAction, SitepingPosition, SitepingPublicEventListener, SitepingPublicEvents, SitepingSkipReason, SitepingStore, SitepingStoreConfig, SitepingTheme, SitepingUnsubscribe, } from "./types.cjs";
|
|
19
|
+
export { ANCHOR_ELEMENT_ID_MAX, ANCHOR_ELEMENT_TAG_MAX, BUILTIN_LOCALES, CLOSED_FEEDBACK_STATUSES, COMMENT_AUTHOR_ROLES, COMMENT_BODY_MAX_LENGTH, CONSOLE_DIAGNOSTIC_LEVELS, FEEDBACK_STATUSES, FEEDBACK_TYPES, flattenAnnotation, IDENTITY_FIELD_MAX_LENGTH, isClosedStatus, isStoreDuplicate, isStoreLimit, isStoreNotFound, isStorePersistence, isStoreValueTooLong, MAX_COMMENTS_PER_FEEDBACK, OPEN_FEEDBACK_STATUSES, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, StoreValueTooLongError, toFeedbackUpdate, } from "./types.cjs";
|
|
20
|
+
export { errorFromResponse, feedbackQueryToSearchParams, isCommentGone, isThreadFull, mergeRequestHeaders, networkErrorFromException, newClientId, withSearchParams, } from "./wire.cjs";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { settleWithConcurrencyLimit } from "./concurrency.js";
|
|
2
|
+
export { buildDeepLink, parseHttpUrl } from "./deep-link.js";
|
|
3
|
+
export { EMAIL_PATTERN, isValidEmail } from "./email.js";
|
|
4
|
+
export type { SitepingErrorCode } from "./errors.js";
|
|
5
|
+
export { SitepingAuthError, SitepingError, SitepingNetworkError, SitepingValidationError } from "./errors.js";
|
|
6
|
+
export type { FilterResult, Pagination } from "./filters.js";
|
|
7
|
+
export { applyFeedbackFilters, clampPagination, DEFAULT_PAGE_LIMIT, isUnreachableOffset, MAX_PAGE_LIMIT, matchesFeedbackQuery, } from "./filters.js";
|
|
8
|
+
export type { I18n, LocaleLoaders, TranslateFunction } from "./i18n.js";
|
|
9
|
+
export { canonicalizeLocale, createI18n, interpolate, intlLocale, tWithParams } from "./i18n.js";
|
|
10
|
+
export type { FieldDef, IndexDef, ModelDef, PrismaNativeType, PrismaScalarType, RelationDef, RelationKind, RelationOnDelete, SitepingModelFieldName, SitepingModelName, } from "./schema.js";
|
|
11
|
+
export { isRelationField, isScalarField, SITEPING_MODELS } from "./schema.js";
|
|
12
|
+
export type { ScreenshotStorage } from "./screenshot-storage.js";
|
|
13
|
+
export { SCREENSHOT_DELETE_CONCURRENCY, screenshotMimeType } from "./screenshot-storage.js";
|
|
14
|
+
export type { CollectionStore, CollectionStoreBackend } from "./store-helpers.js";
|
|
15
|
+
export { buildAnnotationRecord, buildCommentRecord, buildFeedbackRecord, createCollectionStore, } from "./store-helpers.js";
|
|
16
|
+
export type { AssertEqual, IfEquals, Prettify, Serialized } from "./type-utils.js";
|
|
17
|
+
export { hasOwn, isRecord } from "./type-utils.js";
|
|
18
|
+
export type { AnchorData, AnnotationCreateInput, AnnotationPayload, AnnotationRecord, AnnotationResponse, BuiltinLocale, ClosedFeedbackStatus, CommentAuthorRole, CommentCreateInput, CommentDeletePayload, CommentPayload, CommentRecord, CommentResponse, ConsoleDiagnosticEntry, ConsoleDiagnosticLevel, DiagnosticsCaptureOptions, DiagnosticsSnapshot, FeedbackCreateInput, FeedbackCreateOutcome, FeedbackListPermissions, FeedbackPage, FeedbackPayload, FeedbackPermissions, FeedbackQuery, FeedbackRecord, FeedbackResponse, FeedbackResponseList, FeedbackStatus, FeedbackType, FeedbackUpdateInput, NetworkDiagnosticEntry, OpenFeedbackStatus, PageScope, RectData, ScreenshotRegion, SitepingBaseConfig, SitepingCapabilities, SitepingConfig, SitepingDeepLinkOptions, SitepingHeadersOption, SitepingHttpConfig, SitepingIdentity, SitepingInstance, SitepingLocale, SitepingPanelAction, SitepingPanelActionContext, SitepingPanelActionFeedback, SitepingPanelButtonAction, SitepingPanelLinkAction, SitepingPosition, SitepingPublicEventListener, SitepingPublicEvents, SitepingSkipReason, SitepingStore, SitepingStoreConfig, SitepingTheme, SitepingUnsubscribe, } from "./types.js";
|
|
19
|
+
export { ANCHOR_ELEMENT_ID_MAX, ANCHOR_ELEMENT_TAG_MAX, BUILTIN_LOCALES, CLOSED_FEEDBACK_STATUSES, COMMENT_AUTHOR_ROLES, COMMENT_BODY_MAX_LENGTH, CONSOLE_DIAGNOSTIC_LEVELS, FEEDBACK_STATUSES, FEEDBACK_TYPES, flattenAnnotation, IDENTITY_FIELD_MAX_LENGTH, isClosedStatus, isStoreDuplicate, isStoreLimit, isStoreNotFound, isStorePersistence, isStoreValueTooLong, MAX_COMMENTS_PER_FEEDBACK, OPEN_FEEDBACK_STATUSES, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, StoreValueTooLongError, toFeedbackUpdate, } from "./types.js";
|
|
20
|
+
export { errorFromResponse, feedbackQueryToSearchParams, isCommentGone, isThreadFull, mergeRequestHeaders, networkErrorFromException, newClientId, withSearchParams, } from "./wire.js";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record-construction helpers and the collection-store engine.
|
|
3
|
+
*
|
|
4
|
+
* Every snapshot-style adapter (memory, localStorage, flat file, KV, …)
|
|
5
|
+
* needs the same three ingredients: turn a `FeedbackCreateInput` into a
|
|
6
|
+
* `FeedbackRecord` (null-normalizing optional fields, stamping ids and
|
|
7
|
+
* timestamps), filter/paginate with `applyFeedbackFilters`, and implement
|
|
8
|
+
* the dedup/update/delete choreography of the `SitepingStore` contract.
|
|
9
|
+
*
|
|
10
|
+
* `buildFeedbackRecord` / `buildAnnotationRecord` / `buildCommentRecord`
|
|
11
|
+
* cover the first part for any adapter. `createCollectionStore` covers all
|
|
12
|
+
* of it: give it `load`, `persist`, and `generateId`, and it returns a fully
|
|
13
|
+
* conformant `SitepingStore` — writing a new snapshot adapter is ~20 lines
|
|
14
|
+
* plus its storage specifics.
|
|
15
|
+
*/
|
|
16
|
+
import type { AnnotationCreateInput, AnnotationRecord, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord, SitepingStore } from "./types.cjs";
|
|
17
|
+
/**
|
|
18
|
+
* Build a persisted `AnnotationRecord` from its create input — normalizes
|
|
19
|
+
* the optional anchor fields to `null` and stamps identity/timestamp.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildAnnotationRecord(input: AnnotationCreateInput, ctx: {
|
|
22
|
+
id: string;
|
|
23
|
+
feedbackId: string;
|
|
24
|
+
now: Date;
|
|
25
|
+
}): AnnotationRecord;
|
|
26
|
+
/** Build a persisted `CommentRecord` from its create input — stamps identity, thread and timestamp. */
|
|
27
|
+
export declare function buildCommentRecord(input: CommentCreateInput, ctx: {
|
|
28
|
+
id: string;
|
|
29
|
+
feedbackId: string;
|
|
30
|
+
now?: Date;
|
|
31
|
+
}): CommentRecord;
|
|
32
|
+
/**
|
|
33
|
+
* Build a persisted `FeedbackRecord` (with its annotations) from a create
|
|
34
|
+
* input — normalizes every optional field to `null` and stamps ids and
|
|
35
|
+
* timestamps. Adapters without external screenshot storage keep the data
|
|
36
|
+
* URL inline on `screenshotUrl`, which is what this helper does; adapters
|
|
37
|
+
* with a `ScreenshotStorage` upload first and override `screenshotUrl`.
|
|
38
|
+
* It leaves `comments` out, so the record minus its `annotations` stays an
|
|
39
|
+
* insertable feedback row; a store with threads returns `comments: []` itself.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: {
|
|
42
|
+
id: string;
|
|
43
|
+
annotationId: () => string;
|
|
44
|
+
now?: Date;
|
|
45
|
+
}): FeedbackRecord;
|
|
46
|
+
/**
|
|
47
|
+
* Storage primitives behind a collection store. `load`/`persist` may be
|
|
48
|
+
* sync or async, so in-memory arrays, localStorage and async KV stores all
|
|
49
|
+
* fit the same three functions. When both are sync, a mutation runs from
|
|
50
|
+
* `load` to `persist` without yielding, so no other code in the realm can
|
|
51
|
+
* write in between; an async backend is only serialized against the
|
|
52
|
+
* engine's own queue.
|
|
53
|
+
*/
|
|
54
|
+
export interface CollectionStoreBackend {
|
|
55
|
+
/**
|
|
56
|
+
* Return the current full snapshot of feedback records. The engine never
|
|
57
|
+
* mutates this array — handing out a live cache is safe.
|
|
58
|
+
*/
|
|
59
|
+
load(): FeedbackRecord[] | Promise<FeedbackRecord[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Persist the full snapshot — always a new array, never the one `load()`
|
|
62
|
+
* returned. Throw `StorePersistenceError` when the write is lost (quota,
|
|
63
|
+
* storage disabled, …) — never swallow the failure. Because the loaded
|
|
64
|
+
* snapshot is left untouched, a throw here leaves a cached `load()` result
|
|
65
|
+
* consistent with durable storage: no phantom record, no half-applied
|
|
66
|
+
* update.
|
|
67
|
+
*/
|
|
68
|
+
persist(feedbacks: FeedbackRecord[]): void | Promise<void>;
|
|
69
|
+
/** Generate a unique id for a new feedback, annotation or comment record. */
|
|
70
|
+
generateId(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Keep discussion threads on the records: the store gains `addComment` and
|
|
73
|
+
* `deleteComment`, and new records start with `comments: []`. Opt in once
|
|
74
|
+
* `load` hands back what `persist` wrote for them, each comment's
|
|
75
|
+
* `createdAt` a `Date` again (a JSON backend revives it like the record's
|
|
76
|
+
* own dates). Off by default, so an adapter written before threads never
|
|
77
|
+
* gains them — over storage never written for them — on an engine update.
|
|
78
|
+
*/
|
|
79
|
+
comments?: boolean | undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A `SitepingStore` with the optional `verifyProjectOwnership` guaranteed —
|
|
83
|
+
* what `createCollectionStore` returns, which also guarantees
|
|
84
|
+
* `createFeedbackIfAbsent`.
|
|
85
|
+
*/
|
|
86
|
+
export type CollectionStore = SitepingStore & Required<Pick<SitepingStore, "verifyProjectOwnership">>;
|
|
87
|
+
/**
|
|
88
|
+
* Build a fully conformant `SitepingStore` on top of a snapshot backend.
|
|
89
|
+
*
|
|
90
|
+
* The engine implements the whole store contract: clientId dedup (idempotent
|
|
91
|
+
* create, with `createFeedbackIfAbsent` reporting inserts), newest-first
|
|
92
|
+
* ordering, the standard filter/pagination pipeline, `StoreNotFoundError` on
|
|
93
|
+
* missing update/delete, project-scoped bulk delete,
|
|
94
|
+
* `verifyProjectOwnership`, and — with `comments: true` — discussion threads
|
|
95
|
+
* (`addComment`, `deleteComment`) kept on each record. The snapshot returned by `load` is
|
|
96
|
+
* never mutated: every write hands `persist` a new array, so a failed write leaves
|
|
97
|
+
* a cached snapshot exactly as it was. When `persist` fails during `createFeedback`
|
|
98
|
+
* and the record carries an inline screenshot, the engine retries once
|
|
99
|
+
* without the screenshot (by far the heaviest field) so the text feedback
|
|
100
|
+
* survives a storage-quota hit; if that also fails, the error propagates —
|
|
101
|
+
* returning the record would claim a success that was never persisted.
|
|
102
|
+
*
|
|
103
|
+
* Mutations (`createFeedbackIfAbsent`, `createFeedback`, `updateFeedback`,
|
|
104
|
+
* `deleteFeedback`, `deleteAllFeedbacks`, `addComment`, `deleteComment`) run
|
|
105
|
+
* one at a time through a queue owned by the returned store, so concurrent
|
|
106
|
+
* calls — the widget's `Promise.all` bulk resolve/delete, a comment posted
|
|
107
|
+
* while its feedback is resolved — never start from the same snapshot and
|
|
108
|
+
* overwrite each other, and `createFeedbackIfAbsent` reports `created: true`
|
|
109
|
+
* exactly once per `clientId`. A failed mutation rejects with its own error
|
|
110
|
+
* and does not block the ones queued after it, but `load` and `persist` must
|
|
111
|
+
* always settle: one that never does stalls every later mutation on the
|
|
112
|
+
* store, so give network-backed primitives a timeout. Reads are not queued: they see the
|
|
113
|
+
* last persisted snapshot. The guarantee is scoped to one store instance in
|
|
114
|
+
* one JS realm — two instances over the same storage (two
|
|
115
|
+
* `LocalStorageStore`s on one key, two browser tabs, several server
|
|
116
|
+
* processes sharing a KV or a file) are not coordinated; a backend that
|
|
117
|
+
* needs that must bring its own atomic primitive (a transaction, a
|
|
118
|
+
* compare-and-set).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* export class MemoryStore implements SitepingStore {
|
|
123
|
+
* private feedbacks: FeedbackRecord[] = [];
|
|
124
|
+
* private readonly store = createCollectionStore({
|
|
125
|
+
* load: () => this.feedbacks,
|
|
126
|
+
* persist: (next) => {
|
|
127
|
+
* this.feedbacks = next;
|
|
128
|
+
* },
|
|
129
|
+
* generateId: () => crypto.randomUUID(),
|
|
130
|
+
* comments: true,
|
|
131
|
+
* });
|
|
132
|
+
* createFeedback = this.store.createFeedback;
|
|
133
|
+
* // …delegate the remaining methods the same way
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export declare function createCollectionStore(backend: CollectionStoreBackend & {
|
|
138
|
+
comments: true;
|
|
139
|
+
}): CollectionStore & Required<Pick<SitepingStore, "createFeedbackIfAbsent" | "addComment" | "deleteComment">>;
|
|
140
|
+
export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore & Required<Pick<SitepingStore, "createFeedbackIfAbsent">>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record-construction helpers and the collection-store engine.
|
|
3
|
+
*
|
|
4
|
+
* Every snapshot-style adapter (memory, localStorage, flat file, KV, …)
|
|
5
|
+
* needs the same three ingredients: turn a `FeedbackCreateInput` into a
|
|
6
|
+
* `FeedbackRecord` (null-normalizing optional fields, stamping ids and
|
|
7
|
+
* timestamps), filter/paginate with `applyFeedbackFilters`, and implement
|
|
8
|
+
* the dedup/update/delete choreography of the `SitepingStore` contract.
|
|
9
|
+
*
|
|
10
|
+
* `buildFeedbackRecord` / `buildAnnotationRecord` / `buildCommentRecord`
|
|
11
|
+
* cover the first part for any adapter. `createCollectionStore` covers all
|
|
12
|
+
* of it: give it `load`, `persist`, and `generateId`, and it returns a fully
|
|
13
|
+
* conformant `SitepingStore` — writing a new snapshot adapter is ~20 lines
|
|
14
|
+
* plus its storage specifics.
|
|
15
|
+
*/
|
|
16
|
+
import type { AnnotationCreateInput, AnnotationRecord, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord, SitepingStore } from "./types.js";
|
|
17
|
+
/**
|
|
18
|
+
* Build a persisted `AnnotationRecord` from its create input — normalizes
|
|
19
|
+
* the optional anchor fields to `null` and stamps identity/timestamp.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildAnnotationRecord(input: AnnotationCreateInput, ctx: {
|
|
22
|
+
id: string;
|
|
23
|
+
feedbackId: string;
|
|
24
|
+
now: Date;
|
|
25
|
+
}): AnnotationRecord;
|
|
26
|
+
/** Build a persisted `CommentRecord` from its create input — stamps identity, thread and timestamp. */
|
|
27
|
+
export declare function buildCommentRecord(input: CommentCreateInput, ctx: {
|
|
28
|
+
id: string;
|
|
29
|
+
feedbackId: string;
|
|
30
|
+
now?: Date;
|
|
31
|
+
}): CommentRecord;
|
|
32
|
+
/**
|
|
33
|
+
* Build a persisted `FeedbackRecord` (with its annotations) from a create
|
|
34
|
+
* input — normalizes every optional field to `null` and stamps ids and
|
|
35
|
+
* timestamps. Adapters without external screenshot storage keep the data
|
|
36
|
+
* URL inline on `screenshotUrl`, which is what this helper does; adapters
|
|
37
|
+
* with a `ScreenshotStorage` upload first and override `screenshotUrl`.
|
|
38
|
+
* It leaves `comments` out, so the record minus its `annotations` stays an
|
|
39
|
+
* insertable feedback row; a store with threads returns `comments: []` itself.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: {
|
|
42
|
+
id: string;
|
|
43
|
+
annotationId: () => string;
|
|
44
|
+
now?: Date;
|
|
45
|
+
}): FeedbackRecord;
|
|
46
|
+
/**
|
|
47
|
+
* Storage primitives behind a collection store. `load`/`persist` may be
|
|
48
|
+
* sync or async, so in-memory arrays, localStorage and async KV stores all
|
|
49
|
+
* fit the same three functions. When both are sync, a mutation runs from
|
|
50
|
+
* `load` to `persist` without yielding, so no other code in the realm can
|
|
51
|
+
* write in between; an async backend is only serialized against the
|
|
52
|
+
* engine's own queue.
|
|
53
|
+
*/
|
|
54
|
+
export interface CollectionStoreBackend {
|
|
55
|
+
/**
|
|
56
|
+
* Return the current full snapshot of feedback records. The engine never
|
|
57
|
+
* mutates this array — handing out a live cache is safe.
|
|
58
|
+
*/
|
|
59
|
+
load(): FeedbackRecord[] | Promise<FeedbackRecord[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Persist the full snapshot — always a new array, never the one `load()`
|
|
62
|
+
* returned. Throw `StorePersistenceError` when the write is lost (quota,
|
|
63
|
+
* storage disabled, …) — never swallow the failure. Because the loaded
|
|
64
|
+
* snapshot is left untouched, a throw here leaves a cached `load()` result
|
|
65
|
+
* consistent with durable storage: no phantom record, no half-applied
|
|
66
|
+
* update.
|
|
67
|
+
*/
|
|
68
|
+
persist(feedbacks: FeedbackRecord[]): void | Promise<void>;
|
|
69
|
+
/** Generate a unique id for a new feedback, annotation or comment record. */
|
|
70
|
+
generateId(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Keep discussion threads on the records: the store gains `addComment` and
|
|
73
|
+
* `deleteComment`, and new records start with `comments: []`. Opt in once
|
|
74
|
+
* `load` hands back what `persist` wrote for them, each comment's
|
|
75
|
+
* `createdAt` a `Date` again (a JSON backend revives it like the record's
|
|
76
|
+
* own dates). Off by default, so an adapter written before threads never
|
|
77
|
+
* gains them — over storage never written for them — on an engine update.
|
|
78
|
+
*/
|
|
79
|
+
comments?: boolean | undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A `SitepingStore` with the optional `verifyProjectOwnership` guaranteed —
|
|
83
|
+
* what `createCollectionStore` returns, which also guarantees
|
|
84
|
+
* `createFeedbackIfAbsent`.
|
|
85
|
+
*/
|
|
86
|
+
export type CollectionStore = SitepingStore & Required<Pick<SitepingStore, "verifyProjectOwnership">>;
|
|
87
|
+
/**
|
|
88
|
+
* Build a fully conformant `SitepingStore` on top of a snapshot backend.
|
|
89
|
+
*
|
|
90
|
+
* The engine implements the whole store contract: clientId dedup (idempotent
|
|
91
|
+
* create, with `createFeedbackIfAbsent` reporting inserts), newest-first
|
|
92
|
+
* ordering, the standard filter/pagination pipeline, `StoreNotFoundError` on
|
|
93
|
+
* missing update/delete, project-scoped bulk delete,
|
|
94
|
+
* `verifyProjectOwnership`, and — with `comments: true` — discussion threads
|
|
95
|
+
* (`addComment`, `deleteComment`) kept on each record. The snapshot returned by `load` is
|
|
96
|
+
* never mutated: every write hands `persist` a new array, so a failed write leaves
|
|
97
|
+
* a cached snapshot exactly as it was. When `persist` fails during `createFeedback`
|
|
98
|
+
* and the record carries an inline screenshot, the engine retries once
|
|
99
|
+
* without the screenshot (by far the heaviest field) so the text feedback
|
|
100
|
+
* survives a storage-quota hit; if that also fails, the error propagates —
|
|
101
|
+
* returning the record would claim a success that was never persisted.
|
|
102
|
+
*
|
|
103
|
+
* Mutations (`createFeedbackIfAbsent`, `createFeedback`, `updateFeedback`,
|
|
104
|
+
* `deleteFeedback`, `deleteAllFeedbacks`, `addComment`, `deleteComment`) run
|
|
105
|
+
* one at a time through a queue owned by the returned store, so concurrent
|
|
106
|
+
* calls — the widget's `Promise.all` bulk resolve/delete, a comment posted
|
|
107
|
+
* while its feedback is resolved — never start from the same snapshot and
|
|
108
|
+
* overwrite each other, and `createFeedbackIfAbsent` reports `created: true`
|
|
109
|
+
* exactly once per `clientId`. A failed mutation rejects with its own error
|
|
110
|
+
* and does not block the ones queued after it, but `load` and `persist` must
|
|
111
|
+
* always settle: one that never does stalls every later mutation on the
|
|
112
|
+
* store, so give network-backed primitives a timeout. Reads are not queued: they see the
|
|
113
|
+
* last persisted snapshot. The guarantee is scoped to one store instance in
|
|
114
|
+
* one JS realm — two instances over the same storage (two
|
|
115
|
+
* `LocalStorageStore`s on one key, two browser tabs, several server
|
|
116
|
+
* processes sharing a KV or a file) are not coordinated; a backend that
|
|
117
|
+
* needs that must bring its own atomic primitive (a transaction, a
|
|
118
|
+
* compare-and-set).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* export class MemoryStore implements SitepingStore {
|
|
123
|
+
* private feedbacks: FeedbackRecord[] = [];
|
|
124
|
+
* private readonly store = createCollectionStore({
|
|
125
|
+
* load: () => this.feedbacks,
|
|
126
|
+
* persist: (next) => {
|
|
127
|
+
* this.feedbacks = next;
|
|
128
|
+
* },
|
|
129
|
+
* generateId: () => crypto.randomUUID(),
|
|
130
|
+
* comments: true,
|
|
131
|
+
* });
|
|
132
|
+
* createFeedback = this.store.createFeedback;
|
|
133
|
+
* // …delegate the remaining methods the same way
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export declare function createCollectionStore(backend: CollectionStoreBackend & {
|
|
138
|
+
comments: true;
|
|
139
|
+
}): CollectionStore & Required<Pick<SitepingStore, "createFeedbackIfAbsent" | "addComment" | "deleteComment">>;
|
|
140
|
+
export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore & Required<Pick<SitepingStore, "createFeedbackIfAbsent">>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General-purpose TypeScript utility types used across `@siteping/*`.
|
|
3
|
+
*
|
|
4
|
+
* These are kept dependency-free and re-exported from the package entry
|
|
5
|
+
* so adapters and integrators can rely on the same primitives the core
|
|
6
|
+
* uses internally.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Force TypeScript to expand a computed type into a flat object literal in
|
|
10
|
+
* tooltips and error messages. Purely cosmetic — same structural type, just
|
|
11
|
+
* easier to read.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* type Raw = Omit<FeedbackRecord, "annotations"> & { annotations: number };
|
|
15
|
+
* type Pretty = Prettify<Raw>; // displayed as a flat object
|
|
16
|
+
*/
|
|
17
|
+
export type Prettify<T> = {
|
|
18
|
+
[K in keyof T]: T[K];
|
|
19
|
+
} & {};
|
|
20
|
+
/**
|
|
21
|
+
* Returns `Y` when `A` is exactly assignable to `B` and vice-versa,
|
|
22
|
+
* otherwise `N`. Powers compile-time equality assertions.
|
|
23
|
+
*/
|
|
24
|
+
export type IfEquals<A, B, Y = true, N = false> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? Y : N;
|
|
25
|
+
/**
|
|
26
|
+
* Compile-time exact-type guard — resolves to `true` when `Actual` and
|
|
27
|
+
* `Expected` are identical, `never` otherwise. Assign the result to a
|
|
28
|
+
* `const _lock: AssertEqual<A, B> = true;` so any drift becomes a compile
|
|
29
|
+
* error at the declaration site.
|
|
30
|
+
*/
|
|
31
|
+
export type AssertEqual<Actual, Expected> = IfEquals<Actual, Expected, true, never>;
|
|
32
|
+
/**
|
|
33
|
+
* JSON-serialized shape of `T` — the wire form produced by `Response.json()`
|
|
34
|
+
* / `JSON.stringify`: `Date` becomes ISO `string` (nullability preserved),
|
|
35
|
+
* arrays are serialized element-wise, everything else is untouched.
|
|
36
|
+
*
|
|
37
|
+
* Used to derive the `*Response` API types from the `*Record` store types so
|
|
38
|
+
* the two can never drift: add a field to `FeedbackRecord` and
|
|
39
|
+
* `FeedbackResponse` follows automatically.
|
|
40
|
+
*/
|
|
41
|
+
export type Serialized<T> = {
|
|
42
|
+
[K in keyof T]: T[K] extends Date ? string : T[K] extends Date | null ? string | null : T[K] extends (infer U)[] ? Serialized<U>[] : T[K];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Read-only all the way down, for plain data that is deeply frozen at
|
|
46
|
+
* runtime: nested objects get read-only properties, arrays become
|
|
47
|
+
* `readonly` arrays.
|
|
48
|
+
*/
|
|
49
|
+
export type DeepReadonly<T> = T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? {
|
|
50
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
51
|
+
} : T;
|
|
52
|
+
/**
|
|
53
|
+
* Type guard that narrows `value` to a non-null `Record<PropertyKey, unknown>`.
|
|
54
|
+
* Useful when validating arbitrary inputs before reading fields.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isRecord(value: unknown): value is Record<PropertyKey, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Returns true when `value` is an object that exposes the requested key.
|
|
59
|
+
* Type-narrows `value` so the property can be accessed without further
|
|
60
|
+
* casting — a strictly typed replacement for `"k" in obj`.
|
|
61
|
+
*
|
|
62
|
+
* Named after the standardised `Object.hasOwn` helper rather than the
|
|
63
|
+
* legacy `Object.prototype.hasOwnProperty`, which the linter forbids
|
|
64
|
+
* shadowing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function hasOwn<K extends PropertyKey>(value: unknown, key: K): value is Record<K, unknown>;
|