diff --git a/app/config.ts b/app/config.ts index 7912577..d5ff02e 100644 --- a/app/config.ts +++ b/app/config.ts @@ -3,10 +3,21 @@ import type { ContrailConfig } from "../src/index"; export const config: ContrailConfig = { namespace: "rsvp.atmo", jetstreams: ["wss://jetstream1.us-east.bsky.network"], - // spaces: { - // type: "tools.atmo.event.space", - // serviceDid: "did:web:rsvp.atmo", - // }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:rsvp.atmo", + }, + community: { + // 32-byte master key for envelope-encrypting stored credentials (app + // passwords + minted signing/rotation keys). Provide via env: hex or + // base64 encoded. For Cloudflare Workers, wire from env.COMMUNITY_MASTER_KEY + // via a config factory pattern. The placeholder below is fine for `pnpm + // generate` (which never instantiates the cipher) but must be replaced + // before starting the server. + masterKey: + (typeof process !== "undefined" ? process.env.COMMUNITY_MASTER_KEY : undefined) ?? + "placeholder-set-me-before-running-server-not-at-build-time", + }, collections: { event: { collection: "community.lexicon.calendar.event", diff --git a/community-lexicon-templates/adopt.json b/community-lexicon-templates/adopt.json new file mode 100644 index 0000000..1a9441a --- /dev/null +++ b/community-lexicon-templates/adopt.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.adopt", + "defs": { + "main": { + "type": "procedure", + "description": "Adopt an existing ATProto account as a community identity. The provided app password is verified by creating a session, then stored encrypted. Creates the reserved `$admin` and `$publishers` spaces with the caller as `owner` in both.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["identifier", "appPassword"], + "properties": { + "identifier": { "type": "string", "description": "Handle or DID of the account to adopt." }, + "appPassword": { "type": "string", "description": "App password for the account; verified by createSession and stored encrypted." } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid"], + "properties": { + "communityDid": { "type": "string", "format": "did" } + } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "Unauthorized" }, + { "name": "AlreadyExists" } + ] + } + } +} diff --git a/community-lexicon-templates/defs.json b/community-lexicon-templates/defs.json new file mode 100644 index 0000000..680d394 --- /dev/null +++ b/community-lexicon-templates/defs.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.defs", + "description": "Shared types for community-module XRPC methods.", + "defs": { + "accessLevel": { + "type": "string", + "knownValues": ["member", "manager", "admin", "owner"], + "description": "Module-internal access levels that govern community operations on a given space. Ordered: member < manager < admin < owner." + }, + "communityView": { + "type": "object", + "required": ["did", "mode", "createdAt"], + "properties": { + "did": { "type": "string", "format": "did" }, + "mode": { "type": "string", "knownValues": ["adopt", "mint"] }, + "identifier": { "type": "string", "description": "Handle or DID at adoption time (adopt only)." }, + "createdAt": { "type": "integer" } + } + }, + "spaceView": { + "type": "object", + "required": ["uri", "ownerDid", "type", "key", "serviceDid", "createdAt"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "ownerDid": { "type": "string", "format": "did" }, + "type": { "type": "string", "format": "nsid" }, + "key": { "type": "string" }, + "serviceDid": { "type": "string" }, + "createdAt": { "type": "integer" } + } + }, + "subject": { + "type": "object", + "description": "Exactly one of `did` or `spaceUri` must be set.", + "properties": { + "did": { "type": "string", "format": "did" }, + "spaceUri": { "type": "string", "format": "at-uri" } + } + }, + "memberRow": { + "type": "object", + "required": ["subject", "accessLevel", "grantedBy", "grantedAt"], + "properties": { + "subject": { "type": "ref", "ref": "#subject" }, + "accessLevel": { "type": "ref", "ref": "#accessLevel" }, + "grantedBy": { "type": "string", "format": "did" }, + "grantedAt": { "type": "integer" } + } + }, + "flatMember": { + "type": "object", + "required": ["did", "addedAt"], + "properties": { + "did": { "type": "string", "format": "did" }, + "addedAt": { "type": "integer" } + } + } + } +} diff --git a/community-lexicon-templates/delete.json b/community-lexicon-templates/delete.json new file mode 100644 index 0000000..dffb31c --- /dev/null +++ b/community-lexicon-templates/delete.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.delete", + "defs": { + "main": { + "type": "procedure", + "description": "Soft-delete a community. Also soft-deletes all spaces owned by the community. Caller must have `owner` in the community's `$admin` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid"], + "properties": { + "communityDid": { "type": "string", "format": "did" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/deleteRecord.json b/community-lexicon-templates/deleteRecord.json new file mode 100644 index 0000000..cd10cad --- /dev/null +++ b/community-lexicon-templates/deleteRecord.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a public record authored by the community from the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid", "collection", "rkey"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" }, + { "name": "NotSupported" }, + { "name": "UpstreamFailure" } + ] + } + } +} diff --git a/community-lexicon-templates/getHealth.json b/community-lexicon-templates/getHealth.json new file mode 100644 index 0000000..4994700 --- /dev/null +++ b/community-lexicon-templates/getHealth.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.getHealth", + "defs": { + "main": { + "type": "query", + "description": "Check whether stored credentials for the community are still usable. Adopted: attempts a live session creation. Minted: verifies the signing key can be decrypted. Caller must have at least `member` in the community's `$admin` space.", + "parameters": { + "type": "params", + "required": ["communityDid"], + "properties": { + "communityDid": { "type": "string", "format": "did" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "knownValues": ["healthy", "degraded", "expired"] + } + } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/list.json b/community-lexicon-templates/list.json new file mode 100644 index 0000000..abff4f0 --- /dev/null +++ b/community-lexicon-templates/list.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.list", + "defs": { + "main": { + "type": "query", + "description": "List communities where the actor has any access level in any community-owned space. Defaults to the JWT issuer when `actor` is omitted.", + "parameters": { + "type": "params", + "properties": { + "actor": { "type": "string", "format": "did", "description": "DID to query (defaults to the JWT issuer)." } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communities"], + "properties": { + "communities": { + "type": "array", + "items": { "type": "ref", "ref": "tools.atmo.community.defs#communityView" } + } + } + } + } + } + } +} diff --git a/community-lexicon-templates/mint.json b/community-lexicon-templates/mint.json new file mode 100644 index 0000000..303b703 --- /dev/null +++ b/community-lexicon-templates/mint.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.mint", + "defs": { + "main": { + "type": "procedure", + "description": "Mint a fresh did:plc for a new community. Contrail generates three P-256 keypairs (signing, contrail rotation, creator rotation), submits a genesis op to the PLC directory, and stores the signing + contrail-rotation keys encrypted. The creator's rotation key is returned once as a recovery secret and never stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "handle": { "type": "string", "description": "Optional handle to include in alsoKnownAs (as at://)." }, + "pdsEndpoint": { "type": "string", "description": "Optional PDS endpoint to include as the atproto_pds service." } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid", "recoveryKey"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "recoveryKey": { "type": "unknown", "description": "Creator's rotation key as a private JWK. Shown once — cannot be retrieved later." } + } + } + }, + "errors": [ + { "name": "UpstreamFailure" } + ] + } + } +} diff --git a/community-lexicon-templates/putRecord.json b/community-lexicon-templates/putRecord.json new file mode 100644 index 0000000..4f8ae01 --- /dev/null +++ b/community-lexicon-templates/putRecord.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Publish a public record authored by the community via the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid", "collection", "record"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" }, + "record": { "type": "unknown" }, + "validate": { "type": "boolean" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } + } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" }, + { "name": "NotSupported" }, + { "name": "UpstreamFailure" } + ] + } + } +} diff --git a/community-lexicon-templates/reauth.json b/community-lexicon-templates/reauth.json new file mode 100644 index 0000000..bc5d441 --- /dev/null +++ b/community-lexicon-templates/reauth.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.reauth", + "defs": { + "main": { + "type": "procedure", + "description": "Replace the stored app password for an adopted community. Caller must have `owner` access in the community's `$admin` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid", "appPassword"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "appPassword": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" }, + { "name": "Unauthorized" }, + { "name": "NotSupported" } + ] + } + } +} diff --git a/community-lexicon-templates/space/create.json b/community-lexicon-templates/space/create.json new file mode 100644 index 0000000..1210cea --- /dev/null +++ b/community-lexicon-templates/space/create.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create a community-owned space (group/role/channel). Caller needs `admin` or higher in the community's `$admin` space. Reserved keys (`$admin`, `$publishers`, …) are rejected here.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["communityDid"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "key": { "type": "string", "description": "Space key. Auto-generated if omitted." } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["space"], + "properties": { + "space": { "type": "ref", "ref": "tools.atmo.community.defs#spaceView" } + } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" }, + { "name": "AlreadyExists" } + ] + } + } +} diff --git a/community-lexicon-templates/space/delete.json b/community-lexicon-templates/space/delete.json new file mode 100644 index 0000000..6adc5f7 --- /dev/null +++ b/community-lexicon-templates/space/delete.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.delete", + "defs": { + "main": { + "type": "procedure", + "description": "Soft-delete a community-owned space. Caller must be `owner` on the space, or `admin`+ in the community's `$admin` space. Reserved spaces cannot be deleted.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/deleteRecord.json b/community-lexicon-templates/space/deleteRecord.json new file mode 100644 index 0000000..5f6da1f --- /dev/null +++ b/community-lexicon-templates/space/deleteRecord.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete an in-space record authored by the community DID. Caller must be `admin` or higher in the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "collection", "rkey"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/grant.json b/community-lexicon-templates/space/grant.json new file mode 100644 index 0000000..b8d1ffa --- /dev/null +++ b/community-lexicon-templates/space/grant.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.grant", + "defs": { + "main": { + "type": "procedure", + "description": "Grant or upsert a subject's access level on a space. Caller needs `manager` or higher; the granted level cannot exceed the caller's own. A subject can be a DID (member) or another space (delegated membership).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "subject", "accessLevel"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "subject": { "type": "ref", "ref": "tools.atmo.community.defs#subject" }, + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/listMembers.json b/community-lexicon-templates/space/listMembers.json new file mode 100644 index 0000000..26132e2 --- /dev/null +++ b/community-lexicon-templates/space/listMembers.json @@ -0,0 +1,40 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.listMembers", + "defs": { + "main": { + "type": "query", + "description": "List members of a community-owned space. By default returns the raw access-level rows; with `flatten=true` returns the flattened DID list (delegated memberships resolved).", + "parameters": { + "type": "params", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "flatten": { "type": "boolean", "description": "If true, return the flat DID list instead of raw rows." } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { "type": "ref", "ref": "tools.atmo.community.defs#memberRow" }, + "description": "Present when flatten=false." + }, + "members": { + "type": "array", + "items": { "type": "ref", "ref": "tools.atmo.community.defs#flatMember" }, + "description": "Present when flatten=true." + } + } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/putRecord.json b/community-lexicon-templates/space/putRecord.json new file mode 100644 index 0000000..aa2eb35 --- /dev/null +++ b/community-lexicon-templates/space/putRecord.json @@ -0,0 +1,40 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Write an in-space record authored by the community DID (rather than the caller). Caller must be `admin` or higher in the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "collection", "record"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" }, + "record": { "type": "unknown" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["rkey", "authorDid", "createdAt"], + "properties": { + "rkey": { "type": "string" }, + "authorDid": { "type": "string", "format": "did" }, + "createdAt": { "type": "integer" } + } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/resync.json b/community-lexicon-templates/space/resync.json new file mode 100644 index 0000000..cf23c72 --- /dev/null +++ b/community-lexicon-templates/space/resync.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.resync", + "defs": { + "main": { + "type": "procedure", + "description": "Manually recompute `spaces_members` for a community-owned space. Useful after a crash mid-reconcile leaves stale membership. Caller must have `admin` or higher.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/revoke.json b/community-lexicon-templates/space/revoke.json new file mode 100644 index 0000000..e804ad3 --- /dev/null +++ b/community-lexicon-templates/space/revoke.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Remove a subject's access grant from a space. Caller must have `manager` or higher and outrank the subject's current level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "subject"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "subject": { "type": "ref", "ref": "tools.atmo.community.defs#subject" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/space/setAccessLevel.json b/community-lexicon-templates/space/setAccessLevel.json new file mode 100644 index 0000000..090d096 --- /dev/null +++ b/community-lexicon-templates/space/setAccessLevel.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.space.setAccessLevel", + "defs": { + "main": { + "type": "procedure", + "description": "Change the access level of an existing grant. Caller must outrank both the old and new level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "subject", "accessLevel"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "subject": { "type": "ref", "ref": "tools.atmo.community.defs#subject" }, + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/community-lexicon-templates/whoami.json b/community-lexicon-templates/whoami.json new file mode 100644 index 0000000..bf5e847 --- /dev/null +++ b/community-lexicon-templates/whoami.json @@ -0,0 +1,28 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.whoami", + "defs": { + "main": { + "type": "query", + "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", + "parameters": { + "type": "params", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel", "description": "Null when caller has no resolvable access." } + } + } + } + } + } +} diff --git a/docs/community-spec-mapping.md b/docs/community-spec-mapping.md new file mode 100644 index 0000000..2b1fd41 --- /dev/null +++ b/docs/community-spec-mapping.md @@ -0,0 +1,135 @@ +# Communities: mapping to the Arbiter design post + +This is the map between contrail's community module and the Arbiter design +sketched at (zicklag / Roomy, +April 2026). The post is an early design note and will likely evolve — so +will this doc. The goal is to make it obvious, when the standard firms up, +where contrail already lines up and where it needs to change. + +Contrail's community module layers community-owned spaces and tiered +access-level management on top of the [spaces](./spaces-spec-mapping.md) +module. Ownership alone — spaces owned by a community DID — is the signal +that a space is community-managed. All the Arbiter's "group management" +complexity lives here; the spaces module stays close to the rough +permissioned-data spec with no knowledge of access levels or delegation. + +--- + +## Concept-by-concept alignment + +| Arbiter concept | Contrail | Alignment | Notes | +| ------------------------------------------- | ------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------- | +| Community as a DID | `communities.did` | ✅ | 1:1 | +| Mint a fresh did:plc for a community | `community.mint` → P-256 keypairs + genesis op + plc.directory POST | ✅ | Post uses secp256k1; we use P-256 (both spec-valid, we avoid the dep) | +| Adopt an existing account | `community.adopt` (app password) | ➕ | Not in the post; contrail addition. See [community.md](./community.md) for the rationale | +| Creator-held rotation key (recovery) | Returned once by `community.mint` as `recoveryKey` | ✅ | Never stored; caller must save it | +| Groups-are-spaces | Community-owned rows in `spaces` | ✅ | No separate groups table; every group is a space | +| `$admin` reserved space | Auto-created on community creation | ✅ | Keyed by the literal string `$admin`; cannot be deleted | +| `$publishers` reserved space | Auto-created on community creation | ➕ | Not in the post; contrail extension for the "publish public records as the community" capability | +| Delegated membership (space → space) | `community_access_levels.subject_kind = 'space'` | ✅ | Recursive resolution with cycle guard + depth cap | +| Access-level ladder | `member` / `manager` / `admin` / `owner` | ⚠️ | 4 levels vs the post's 8. See [community.md § Relationship to the Arbiter post](./community.md) | +| Read-Member-List (pre-member tier) | _none_ | ❌ | Post's level 1; skipped in v1 | +| Add-Members vs Remove-Members split | Bundled into `manager` | ⚠️ | Post treats them separately | +| Configure-Space | `admin` in that space | ✅ | Post's level 5 | +| Create-Spaces in `$admin` | `admin` in `$admin` | ✅ | Post's level 6 | +| Remove-Space | `owner` in target space OR `admin` in `$admin` | ✅ | Post's level 7 | +| Owner | `owner`; only meaningful in `$admin` for owner-management | ✅ | Post's level 8 | +| Push model for membership lists | Reconciler writes `spaces_members` after each change | ➕ | Post doesn't specify a sync direction; push keeps spaces read-path zero-overhead | +| Cross-community / cross-arbiter delegation | _same-contrail only_ | ❌ | v1 constraint. Private-membership federation is deferred — see [community.md § Deferred work] | +| Invites as a separate service | _not implemented_ | ❌ | Post proposes an optional add-on; contrail has spaces invites to draw from when this lands | +| Writing records under the arbiter's account | `community.space.putRecord` (in-space) + `community.putRecord` (public) | ✅ | In-space: `admin+`. Public: `member+` in `$publishers` — routes through adopted community's PDS | +| Public membership-list flag | _none_ | ❌ | Post allows spaces to expose membership publicly. Ruled out for cross-instance federation (privacy) | +| Space credential (from arbiter) | _none_ | ⚠️ | Covered by the spaces module's service-auth story, not the community module | + +Legend: ✅ aligned · ⚠️ partial / different granularity · ❌ missing · ➕ contrail extension (not in the post) + +--- + +## XRPC surface + +All endpoints emitted under `.community.*` from templates +in `community-lexicon-templates/`. Distinct from `.space.*` so the +user-managed spaces surface stays clean. Community-managed spaces live +under `.community.space.*` so the endpoint name reveals whether +membership is user-controlled or community-controlled. + +### Community lifecycle +- `community.adopt` · `community.mint` · `community.reauth` · `community.delete` +- `community.list` · `community.getHealth` · `community.whoami` + +### Space (group / role / channel) lifecycle +- `community.space.create` · `community.space.delete` + +### Membership +- `community.space.grant` · `community.space.revoke` · `community.space.setAccessLevel` +- `community.space.listMembers` (`?flatten=true` for the resolved DID list) · `community.space.resync` + +### Publishing +- `community.putRecord` · `community.deleteRecord` (public records via the community's PDS; adopted only) +- `community.space.putRecord` · `community.space.deleteRecord` (in-space records authored by the community DID) + +--- + +## Contrail extensions over the post + +Labelled ➕ in the alignment table, worth calling out explicitly so +future renames against a standard are easy: + +- **`adopt` mode** for communities backed by a regular ATProto account. + App passwords are the default because they don't have the periodic + re-auth ceremony OAuth does, which matters for machine-operated + community accounts. +- **`$publishers` reserved space.** The post doesn't model "publish public + records as the community" — we gate that capability via a second + reserved space so it's orthogonal to `$admin` governance without + widening the access ladder. +- **Push-based reconciliation into `spaces_members`.** The post doesn't + specify sync direction; push keeps the spaces read path O(1) and lets + the spaces module stay oblivious to delegation. + +--- + +## Migration readiness + +Hasn't shipped as a standard yet. When it does, likely churn areas: + +1. **Level renames / additions.** Our 4 are a coalesce of the post's 8. + If a standard lands with finer tiers, add rows to the enum and + migrate existing values — `community_access_levels.access_level` is + a plain text column. +2. **Cross-instance delegation.** Our v1 is local-only. When private + cross-contrail federation is figured out (encrypted membership + records, authenticated pull, something else), the `subject_space_uri` + field already supports remote URIs structurally — only the + resolution path needs to change. +3. **Invites.** When the post's invite service firms up, reuse the + spaces invite primitive or fork it. Adding an invite service as a + member of `$admin` with `manager` access is the model; no schema + change anticipated. +4. **Publishing for minted communities.** Currently returns + `NotSupported`. When contrail-as-PDS or PDS-less minted publishing + is defined, drop the check. + +--- + +## Design decisions worth preserving + +- **Spaces doesn't know the community module exists.** Dependency is + one-way: community writes to spaces tables; the spaces module never + imports from `src/core/community/`. Preserve this — it's what keeps + the spaces surface close to the permissioned-data spec. +- **Ownership is the signal.** Community-owned = owner DID is in + `communities`. No flag columns on `spaces`. Preserve this — adding a + delegation flag re-introduces a schema hook we deliberately dropped. +- **Access levels govern arbiter operations only.** Never leak them + into record-level ACLs or app-level role systems. App roles belong + as records, not as levels. +- **Orthogonal capabilities go in reserved `$`-spaces, not new + ladder rungs.** `$publishers` is the first; `$moderators`, + `$billing`, etc. can follow. This keeps the ladder small and + extensions additive. +- **Push, not pull, for `spaces_members`.** Read-path overhead has to + stay zero; complexity belongs in the writer. +- **Single-instance is a feature, not a gap.** Federation is the + hard problem; defer cleanly with a local-only assertion at grant + time until private-distribution shape is clear. diff --git a/docs/spaces-spec-mapping.md b/docs/spaces-spec-mapping.md index 890f193..36c9ccf 100644 --- a/docs/spaces-spec-mapping.md +++ b/docs/spaces-spec-mapping.md @@ -21,7 +21,7 @@ flow is shipped (same story we already have for public records via jetstream). | Space type (NSID) | `spaces.type` | ✅ | 1:1 | | Space key / skey | `spaces.key` | ✅ | TID-generated when caller omits it | | Record addressing 6-tuple | `(owner, type, key, author-did, collection, rkey)` | ✅ | Storage is keyed by `(space_uri, did, rkey)`; `space_uri` encodes the first 3 | -| Single ACL = member list | `spaces_members (did, perms)` | ✅ | Only `read`/`write` perms; owner is implicit write | +| Single ACL = member list | `spaces_members (did)` | ✅ | Membership is binary: you're in or you're out. Owner is implicit member. No read/write tiering — apps filter writes themselves | | Space credential (2–4h token) | _none; service-auth JWTs used directly_ | ❌ | Fine while contrail is a single appview. Add a shim when real PDS sync lands | | App allow/deny | `appPolicy {mode, apps[]}` | ✅ | Matches spec's default-allow / default-deny model. Visible only to the owner | | Permissioned repo per user | single DB (`spaces_records_`) | ⚠️ | Structurally compatible — keyed per `(space, author)`. Federation is future | @@ -99,6 +99,17 @@ the real spec lands: - Keep the member list as the single ACL. Don't add roles or per-collection policies just because it's easy — the spec is emphatic that the member list is _the_ ACL. +- Membership is binary, not tiered. Previously had `perms: "read" | "write"` + per member row; collapsed to plain membership because the rough spec is + moving toward "member = access, apps filter writes." Delete keeps the + owner / own-record rule, but that's about *which records you can affect*, + not a permission tier on the member row. +- Don't over-engineer the space row with pre-emptive extension columns. + Previously had `member_list_ref` as a hook for externally-managed + membership; dropped because the community-module case is handled via + ownership (community-owned spaces are managed by the community module, no + flag column needed). If a future need for external membership sources + shows up, add the column then. - Keep `space.whoami`, `space.leaveSpace`, and the invite endpoints clearly labeled as contrail extras in docs. If the spec ends up naming some of them, renaming is cheap; relying on them from the base spec isn't. diff --git a/lexicons-generated/rsvp/atmo/community/adopt.json b/lexicons-generated/rsvp/atmo/community/adopt.json new file mode 100644 index 0000000..9351b1e --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/adopt.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.adopt", + "defs": { + "main": { + "type": "procedure", + "description": "Adopt an existing ATProto account as a community identity. The provided app password is verified by creating a session, then stored encrypted. Creates the reserved `$admin` and `$publishers` spaces with the caller as `owner` in both.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "identifier", + "appPassword" + ], + "properties": { + "identifier": { + "type": "string", + "description": "Handle or DID of the account to adopt." + }, + "appPassword": { + "type": "string", + "description": "App password for the account; verified by createSession and stored encrypted." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "Unauthorized" + }, + { + "name": "AlreadyExists" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/defs.json b/lexicons-generated/rsvp/atmo/community/defs.json new file mode 100644 index 0000000..1471660 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/defs.json @@ -0,0 +1,135 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.defs", + "description": "Shared types for community-module XRPC methods.", + "defs": { + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "Module-internal access levels that govern community operations on a given space. Ordered: member < manager < admin < owner." + }, + "communityView": { + "type": "object", + "required": [ + "did", + "mode", + "createdAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "mode": { + "type": "string", + "knownValues": [ + "adopt", + "mint" + ] + }, + "identifier": { + "type": "string", + "description": "Handle or DID at adoption time (adopt only)." + }, + "createdAt": { + "type": "integer" + } + } + }, + "spaceView": { + "type": "object", + "required": [ + "uri", + "ownerDid", + "type", + "key", + "serviceDid", + "createdAt" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "ownerDid": { + "type": "string", + "format": "did" + }, + "type": { + "type": "string", + "format": "nsid" + }, + "key": { + "type": "string" + }, + "serviceDid": { + "type": "string" + }, + "createdAt": { + "type": "integer" + } + } + }, + "subject": { + "type": "object", + "description": "Exactly one of `did` or `spaceUri` must be set.", + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "memberRow": { + "type": "object", + "required": [ + "subject", + "accessLevel", + "grantedBy", + "grantedAt" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "#subject" + }, + "accessLevel": { + "type": "ref", + "ref": "#accessLevel" + }, + "grantedBy": { + "type": "string", + "format": "did" + }, + "grantedAt": { + "type": "integer" + } + } + }, + "flatMember": { + "type": "object", + "required": [ + "did", + "addedAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "addedAt": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/delete.json b/lexicons-generated/rsvp/atmo/community/delete.json new file mode 100644 index 0000000..85b158e --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/delete.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.delete", + "defs": { + "main": { + "type": "procedure", + "description": "Soft-delete a community. Also soft-deletes all spaces owned by the community. Caller must have `owner` in the community's `$admin` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/deleteRecord.json b/lexicons-generated/rsvp/atmo/community/deleteRecord.json new file mode 100644 index 0000000..1b70e29 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/deleteRecord.json @@ -0,0 +1,65 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a public record authored by the community from the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "collection", + "rkey" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "NotSupported" + }, + { + "name": "UpstreamFailure" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/getHealth.json b/lexicons-generated/rsvp/atmo/community/getHealth.json new file mode 100644 index 0000000..f3c03e5 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/getHealth.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.getHealth", + "defs": { + "main": { + "type": "query", + "description": "Check whether stored credentials for the community are still usable. Adopted: attempts a live session creation. Minted: verifies the signing key can be decrypted. Caller must have at least `member` in the community's `$admin` space.", + "parameters": { + "type": "params", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "knownValues": [ + "healthy", + "degraded", + "expired" + ] + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/list.json b/lexicons-generated/rsvp/atmo/community/list.json new file mode 100644 index 0000000..520e45a --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/list.json @@ -0,0 +1,38 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.list", + "defs": { + "main": { + "type": "query", + "description": "List communities where the actor has any access level in any community-owned space. Defaults to the JWT issuer when `actor` is omitted.", + "parameters": { + "type": "params", + "properties": { + "actor": { + "type": "string", + "format": "did", + "description": "DID to query (defaults to the JWT issuer)." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communities" + ], + "properties": { + "communities": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#communityView" + } + } + } + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/mint.json b/lexicons-generated/rsvp/atmo/community/mint.json new file mode 100644 index 0000000..ab2aea0 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/mint.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.mint", + "defs": { + "main": { + "type": "procedure", + "description": "Mint a fresh did:plc for a new community. Contrail generates three P-256 keypairs (signing, contrail rotation, creator rotation), submits a genesis op to the PLC directory, and stores the signing + contrail-rotation keys encrypted. The creator's rotation key is returned once as a recovery secret and never stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "handle": { + "type": "string", + "description": "Optional handle to include in alsoKnownAs (as at://)." + }, + "pdsEndpoint": { + "type": "string", + "description": "Optional PDS endpoint to include as the atproto_pds service." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "recoveryKey" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "recoveryKey": { + "type": "unknown", + "description": "Creator's rotation key as a private JWK. Shown once — cannot be retrieved later." + } + } + } + }, + "errors": [ + { + "name": "UpstreamFailure" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/putRecord.json b/lexicons-generated/rsvp/atmo/community/putRecord.json new file mode 100644 index 0000000..43581a3 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/putRecord.json @@ -0,0 +1,73 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Publish a public record authored by the community via the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "collection", + "record" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "record": { + "type": "unknown" + }, + "validate": { + "type": "boolean" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "NotSupported" + }, + { + "name": "UpstreamFailure" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/reauth.json b/lexicons-generated/rsvp/atmo/community/reauth.json new file mode 100644 index 0000000..87a05d4 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/reauth.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.reauth", + "defs": { + "main": { + "type": "procedure", + "description": "Replace the stored app password for an adopted community. Caller must have `owner` access in the community's `$admin` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "appPassword" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "appPassword": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "Unauthorized" + }, + { + "name": "NotSupported" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/create.json b/lexicons-generated/rsvp/atmo/community/space/create.json new file mode 100644 index 0000000..8c3bbaa --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/create.json @@ -0,0 +1,58 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create a community-owned space (group/role/channel). Caller needs `admin` or higher in the community's `$admin` space. Reserved keys (`$admin`, `$publishers`, …) are rejected here.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "key": { + "type": "string", + "description": "Space key. Auto-generated if omitted." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "space" + ], + "properties": { + "space": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#spaceView" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "AlreadyExists" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/delete.json b/lexicons-generated/rsvp/atmo/community/space/delete.json new file mode 100644 index 0000000..17fae52 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/delete.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.delete", + "defs": { + "main": { + "type": "procedure", + "description": "Soft-delete a community-owned space. Caller must be `owner` on the space, or `admin`+ in the community's `$admin` space. Reserved spaces cannot be deleted.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json b/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json new file mode 100644 index 0000000..abe2a68 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json @@ -0,0 +1,59 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete an in-space record authored by the community DID. Caller must be `admin` or higher in the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "rkey" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/grant.json b/lexicons-generated/rsvp/atmo/community/space/grant.json new file mode 100644 index 0000000..acca868 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/grant.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.grant", + "defs": { + "main": { + "type": "procedure", + "description": "Grant or upsert a subject's access level on a space. Caller needs `manager` or higher; the granted level cannot exceed the caller's own. A subject can be a DID (member) or another space (delegated membership).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "subject", + "accessLevel" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#subject" + }, + "accessLevel": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#accessLevel" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/listMembers.json b/lexicons-generated/rsvp/atmo/community/space/listMembers.json new file mode 100644 index 0000000..130d72b --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/listMembers.json @@ -0,0 +1,58 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.listMembers", + "defs": { + "main": { + "type": "query", + "description": "List members of a community-owned space. By default returns the raw access-level rows; with `flatten=true` returns the flattened DID list (delegated memberships resolved).", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "flatten": { + "type": "boolean", + "description": "If true, return the flat DID list instead of raw rows." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#memberRow" + }, + "description": "Present when flatten=false." + }, + "members": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#flatMember" + }, + "description": "Present when flatten=true." + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/putRecord.json b/lexicons-generated/rsvp/atmo/community/space/putRecord.json new file mode 100644 index 0000000..eca1cfe --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/putRecord.json @@ -0,0 +1,71 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Write an in-space record authored by the community DID (rather than the caller). Caller must be `admin` or higher in the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "record" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "record": { + "type": "unknown" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "rkey", + "authorDid", + "createdAt" + ], + "properties": { + "rkey": { + "type": "string" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/resync.json b/lexicons-generated/rsvp/atmo/community/space/resync.json new file mode 100644 index 0000000..f0bba3b --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/resync.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.resync", + "defs": { + "main": { + "type": "procedure", + "description": "Manually recompute `spaces_members` for a community-owned space. Useful after a crash mid-reconcile leaves stale membership. Caller must have `admin` or higher.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/revoke.json b/lexicons-generated/rsvp/atmo/community/space/revoke.json new file mode 100644 index 0000000..cff2c18 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/revoke.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Remove a subject's access grant from a space. Caller must have `manager` or higher and outrank the subject's current level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "subject" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#subject" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json b/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json new file mode 100644 index 0000000..1e41342 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.space.setAccessLevel", + "defs": { + "main": { + "type": "procedure", + "description": "Change the access level of an existing grant. Caller must outrank both the old and new level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "subject", + "accessLevel" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#subject" + }, + "accessLevel": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#accessLevel" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/whoami.json b/lexicons-generated/rsvp/atmo/community/whoami.json new file mode 100644 index 0000000..691d361 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/whoami.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.whoami", + "defs": { + "main": { + "type": "query", + "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#accessLevel", + "description": "Null when caller has no resolvable access." + } + } + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/event/getRecord.json b/lexicons-generated/rsvp/atmo/event/getRecord.json index d0343b2..d2dc8a6 100644 --- a/lexicons-generated/rsvp/atmo/event/getRecord.json +++ b/lexicons-generated/rsvp/atmo/event/getRecord.json @@ -310,6 +310,13 @@ "description": "Name of the publication.", "maxGraphemes": 500 }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, "basicTheme": { "ref": "site.standard.theme.basic", "type": "ref", diff --git a/lexicons-generated/rsvp/atmo/event/listRecords.json b/lexicons-generated/rsvp/atmo/event/listRecords.json index e0bba74..3f938e0 100644 --- a/lexicons-generated/rsvp/atmo/event/listRecords.json +++ b/lexicons-generated/rsvp/atmo/event/listRecords.json @@ -424,6 +424,13 @@ "description": "Name of the publication.", "maxGraphemes": 500 }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, "basicTheme": { "ref": "site.standard.theme.basic", "type": "ref", diff --git a/lexicons-generated/rsvp/atmo/getProfile.json b/lexicons-generated/rsvp/atmo/getProfile.json index 622dd6a..d6c6191 100644 --- a/lexicons-generated/rsvp/atmo/getProfile.json +++ b/lexicons-generated/rsvp/atmo/getProfile.json @@ -162,6 +162,13 @@ "description": "Name of the publication.", "maxGraphemes": 500 }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, "basicTheme": { "ref": "site.standard.theme.basic", "type": "ref", diff --git a/lexicons-generated/rsvp/atmo/permissionSet.json b/lexicons-generated/rsvp/atmo/permissionSet.json index c1ab89e..9ac2975 100644 --- a/lexicons-generated/rsvp/atmo/permissionSet.json +++ b/lexicons-generated/rsvp/atmo/permissionSet.json @@ -12,6 +12,24 @@ "resource": "rpc", "aud": "*", "lxm": [ + "rsvp.atmo.community.adopt", + "rsvp.atmo.community.delete", + "rsvp.atmo.community.deleteRecord", + "rsvp.atmo.community.getHealth", + "rsvp.atmo.community.list", + "rsvp.atmo.community.mint", + "rsvp.atmo.community.putRecord", + "rsvp.atmo.community.reauth", + "rsvp.atmo.community.space.create", + "rsvp.atmo.community.space.delete", + "rsvp.atmo.community.space.deleteRecord", + "rsvp.atmo.community.space.grant", + "rsvp.atmo.community.space.listMembers", + "rsvp.atmo.community.space.putRecord", + "rsvp.atmo.community.space.resync", + "rsvp.atmo.community.space.revoke", + "rsvp.atmo.community.space.setAccessLevel", + "rsvp.atmo.community.whoami", "rsvp.atmo.event.getRecord", "rsvp.atmo.event.listRecords", "rsvp.atmo.getCursor", diff --git a/lexicons-generated/rsvp/atmo/rsvp/getRecord.json b/lexicons-generated/rsvp/atmo/rsvp/getRecord.json index 3d6c1c9..d63df47 100644 --- a/lexicons-generated/rsvp/atmo/rsvp/getRecord.json +++ b/lexicons-generated/rsvp/atmo/rsvp/getRecord.json @@ -259,6 +259,13 @@ "description": "Name of the publication.", "maxGraphemes": 500 }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, "basicTheme": { "ref": "site.standard.theme.basic", "type": "ref", diff --git a/lexicons-generated/rsvp/atmo/rsvp/listRecords.json b/lexicons-generated/rsvp/atmo/rsvp/listRecords.json index fb3cb58..d216597 100644 --- a/lexicons-generated/rsvp/atmo/rsvp/listRecords.json +++ b/lexicons-generated/rsvp/atmo/rsvp/listRecords.json @@ -312,6 +312,13 @@ "description": "Name of the publication.", "maxGraphemes": 500 }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, "basicTheme": { "ref": "site.standard.theme.basic", "type": "ref", diff --git a/lexicons-generated/rsvp/atmo/space/addMember.json b/lexicons-generated/rsvp/atmo/space/addMember.json index 3c69ac2..d6308ff 100644 --- a/lexicons-generated/rsvp/atmo/space/addMember.json +++ b/lexicons-generated/rsvp/atmo/space/addMember.json @@ -21,14 +21,6 @@ "did": { "type": "string", "format": "did" - }, - "perms": { - "type": "string", - "knownValues": [ - "read", - "write" - ], - "default": "write" } } } diff --git a/lexicons-generated/rsvp/atmo/space/createSpace.json b/lexicons-generated/rsvp/atmo/space/createSpace.json index f2d6dec..f17d161 100644 --- a/lexicons-generated/rsvp/atmo/space/createSpace.json +++ b/lexicons-generated/rsvp/atmo/space/createSpace.json @@ -19,10 +19,6 @@ "type": "string", "description": "Space key. Auto-generated (TID) if omitted." }, - "memberListRef": { - "type": "string", - "format": "at-uri" - }, "appPolicyRef": { "type": "string", "format": "at-uri" diff --git a/lexicons-generated/rsvp/atmo/space/defs.json b/lexicons-generated/rsvp/atmo/space/defs.json index 1df906e..01f50b6 100644 --- a/lexicons-generated/rsvp/atmo/space/defs.json +++ b/lexicons-generated/rsvp/atmo/space/defs.json @@ -32,10 +32,6 @@ "serviceDid": { "type": "string" }, - "memberListRef": { - "type": "string", - "format": "at-uri" - }, "appPolicyRef": { "type": "string", "format": "at-uri" @@ -54,7 +50,6 @@ "type": "object", "required": [ "did", - "perms", "addedAt" ], "properties": { @@ -62,14 +57,6 @@ "type": "string", "format": "did" }, - "perms": { - "type": "string", - "knownValues": [ - "read", - "write" - ], - "description": "'write' implies 'read'. Space owner is always implicit write." - }, "addedAt": { "type": "integer" }, @@ -146,7 +133,6 @@ "tokenHash", "spaceUri", "kind", - "perms", "usedCount", "createdBy", "createdAt" @@ -167,13 +153,6 @@ "read-join" ] }, - "perms": { - "type": "string", - "knownValues": [ - "read", - "write" - ] - }, "expiresAt": { "type": "integer" }, diff --git a/lexicons-generated/rsvp/atmo/space/invite/create.json b/lexicons-generated/rsvp/atmo/space/invite/create.json index 3eb9e8b..c60f793 100644 --- a/lexicons-generated/rsvp/atmo/space/invite/create.json +++ b/lexicons-generated/rsvp/atmo/space/invite/create.json @@ -27,14 +27,6 @@ "default": "join", "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." }, - "perms": { - "type": "string", - "knownValues": [ - "read", - "write" - ], - "default": "write" - }, "expiresAt": { "type": "integer", "description": "Unix ms timestamp. Omit for no expiry." diff --git a/lexicons-generated/rsvp/atmo/space/invite/redeem.json b/lexicons-generated/rsvp/atmo/space/invite/redeem.json index 9a41ce0..06261e9 100644 --- a/lexicons-generated/rsvp/atmo/space/invite/redeem.json +++ b/lexicons-generated/rsvp/atmo/space/invite/redeem.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Redeem an invite token. The JWT issuer becomes a member of the space with the invite's perms.", + "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", "input": { "encoding": "application/json", "schema": { @@ -24,20 +24,12 @@ "schema": { "type": "object", "required": [ - "spaceUri", - "perms" + "spaceUri" ], "properties": { "spaceUri": { "type": "string", "format": "at-uri" - }, - "perms": { - "type": "string", - "knownValues": [ - "read", - "write" - ] } } } diff --git a/lexicons-generated/rsvp/atmo/space/whoami.json b/lexicons-generated/rsvp/atmo/space/whoami.json index e3737c7..8eb98a2 100644 --- a/lexicons-generated/rsvp/atmo/space/whoami.json +++ b/lexicons-generated/rsvp/atmo/space/whoami.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Report the caller's relationship to a space: whether they are the owner, a member, and at what permission level. Useful for clients to avoid a listMembers roundtrip.", + "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", "parameters": { "type": "params", "required": [ @@ -31,14 +31,6 @@ }, "isMember": { "type": "boolean" - }, - "perms": { - "type": "string", - "knownValues": [ - "read", - "write" - ], - "description": "Present only when the caller is a member or the owner." } } } diff --git a/lexicons-pulled/site/standard/publication.json b/lexicons-pulled/site/standard/publication.json index 456f730..9656716 100644 --- a/lexicons-pulled/site/standard/publication.json +++ b/lexicons-pulled/site/standard/publication.json @@ -25,6 +25,11 @@ "description": "Name of the publication.", "maxGraphemes": 500 }, + "labels": { + "refs": ["com.atproto.label.defs#selfLabels"], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, "basicTheme": { "ref": "site.standard.theme.basic", "type": "ref", @@ -53,8 +58,7 @@ "default": true, "description": "Boolean which decides whether the publication should appear in discovery feeds." } - }, - "description": "Platform-specific preferences for the publication, including discovery and visibility settings." + } } }, "$type": "com.atproto.lexicon.schema", diff --git a/lexicons-pulled/site/standard/theme/basic.json b/lexicons-pulled/site/standard/theme/basic.json index 88c7cba..88ff587 100644 --- a/lexicons-pulled/site/standard/theme/basic.json +++ b/lexicons-pulled/site/standard/theme/basic.json @@ -2,28 +2,32 @@ "id": "site.standard.theme.basic", "defs": { "main": { - "type": "object", - "required": ["background", "foreground", "accent", "accentForeground"], - "properties": { - "accent": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for links and button backgrounds." - }, - "background": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for content background." - }, - "foreground": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for content text." - }, - "accentForeground": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for button text." + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": ["background", "foreground", "accent", "accentForeground"], + "properties": { + "accent": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for links and button backgrounds." + }, + "background": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for content background." + }, + "foreground": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for content text." + }, + "accentForeground": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for button text." + } } }, "description": "A simplified theme definition for publications, providing basic color customization for content display across different platforms and applications." diff --git a/package.json b/package.json index 0370a23..a17637d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "sideEffects": false, "files": [ "dist", - "spaces-lexicon-templates/**/*.json" + "spaces-lexicon-templates/**/*.json", + "community-lexicon-templates/**/*.json" ], "exports": { ".": { @@ -33,7 +34,8 @@ "types": "./dist/publish.d.ts", "import": "./dist/publish.js" }, - "./spaces-lexicon-templates/*.json": "./spaces-lexicon-templates/*.json" + "./spaces-lexicon-templates/*.json": "./spaces-lexicon-templates/*.json", + "./community-lexicon-templates/*.json": "./community-lexicon-templates/*.json" }, "repository": { "type": "git", diff --git a/spaces-lexicon-templates/addMember.json b/spaces-lexicon-templates/addMember.json index e591863..71149a9 100644 --- a/spaces-lexicon-templates/addMember.json +++ b/spaces-lexicon-templates/addMember.json @@ -12,8 +12,7 @@ "required": ["spaceUri", "did"], "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, - "did": { "type": "string", "format": "did" }, - "perms": { "type": "string", "knownValues": ["read", "write"], "default": "write" } + "did": { "type": "string", "format": "did" } } } }, diff --git a/spaces-lexicon-templates/createSpace.json b/spaces-lexicon-templates/createSpace.json index 73a131d..a3f516b 100644 --- a/spaces-lexicon-templates/createSpace.json +++ b/spaces-lexicon-templates/createSpace.json @@ -12,7 +12,6 @@ "properties": { "type": { "type": "string", "format": "nsid", "description": "Space type NSID. Defaults to the service's configured type." }, "key": { "type": "string", "description": "Space key. Auto-generated (TID) if omitted." }, - "memberListRef": { "type": "string", "format": "at-uri" }, "appPolicyRef": { "type": "string", "format": "at-uri" }, "appPolicy": { "type": "ref", "ref": "tools.atmo.space.defs#appPolicy" } } diff --git a/spaces-lexicon-templates/defs.json b/spaces-lexicon-templates/defs.json index 57a5342..4eaee52 100644 --- a/spaces-lexicon-templates/defs.json +++ b/spaces-lexicon-templates/defs.json @@ -12,7 +12,6 @@ "type": { "type": "string", "format": "nsid" }, "key": { "type": "string" }, "serviceDid": { "type": "string" }, - "memberListRef": { "type": "string", "format": "at-uri" }, "appPolicyRef": { "type": "string", "format": "at-uri" }, "createdAt": { "type": "integer" }, "appPolicy": { "type": "ref", "ref": "#appPolicy", "description": "Owner-only" } @@ -20,10 +19,9 @@ }, "memberView": { "type": "object", - "required": ["did", "perms", "addedAt"], + "required": ["did", "addedAt"], "properties": { "did": { "type": "string", "format": "did" }, - "perms": { "type": "string", "knownValues": ["read", "write"], "description": "'write' implies 'read'. Space owner is always implicit write." }, "addedAt": { "type": "integer" }, "addedBy": { "type": "string", "format": "did" } } @@ -51,12 +49,11 @@ }, "inviteView": { "type": "object", - "required": ["tokenHash", "spaceUri", "kind", "perms", "usedCount", "createdBy", "createdAt"], + "required": ["tokenHash", "spaceUri", "kind", "usedCount", "createdBy", "createdAt"], "properties": { "tokenHash": { "type": "string" }, "spaceUri": { "type": "string", "format": "at-uri" }, "kind": { "type": "string", "knownValues": ["join", "read", "read-join"] }, - "perms": { "type": "string", "knownValues": ["read", "write"] }, "expiresAt": { "type": "integer" }, "maxUses": { "type": "integer" }, "usedCount": { "type": "integer" }, diff --git a/spaces-lexicon-templates/invite/create.json b/spaces-lexicon-templates/invite/create.json index 5fa0f55..e3cdf75 100644 --- a/spaces-lexicon-templates/invite/create.json +++ b/spaces-lexicon-templates/invite/create.json @@ -13,7 +13,6 @@ "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, "kind": { "type": "string", "knownValues": ["join", "read", "read-join"], "default": "join", "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." }, - "perms": { "type": "string", "knownValues": ["read", "write"], "default": "write" }, "expiresAt": { "type": "integer", "description": "Unix ms timestamp. Omit for no expiry." }, "maxUses": { "type": "integer", "minimum": 1, "description": "Caps join redemptions only — read-token reads are unlimited. Omit for unlimited joins." }, "note": { "type": "string", "maxLength": 500 } diff --git a/spaces-lexicon-templates/invite/redeem.json b/spaces-lexicon-templates/invite/redeem.json index d3369fa..398127d 100644 --- a/spaces-lexicon-templates/invite/redeem.json +++ b/spaces-lexicon-templates/invite/redeem.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Redeem an invite token. The JWT issuer becomes a member of the space with the invite's perms.", + "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", "input": { "encoding": "application/json", "schema": { @@ -19,10 +19,9 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["spaceUri", "perms"], + "required": ["spaceUri"], "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, - "perms": { "type": "string", "knownValues": ["read", "write"] } + "spaceUri": { "type": "string", "format": "at-uri" } } } }, diff --git a/spaces-lexicon-templates/whoami.json b/spaces-lexicon-templates/whoami.json index e30f2ef..c0e8f9d 100644 --- a/spaces-lexicon-templates/whoami.json +++ b/spaces-lexicon-templates/whoami.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Report the caller's relationship to a space: whether they are the owner, a member, and at what permission level. Useful for clients to avoid a listMembers roundtrip.", + "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", "parameters": { "type": "params", "required": ["spaceUri"], @@ -19,8 +19,7 @@ "required": ["isOwner", "isMember"], "properties": { "isOwner": { "type": "boolean" }, - "isMember": { "type": "boolean" }, - "perms": { "type": "string", "knownValues": ["read", "write"], "description": "Present only when the caller is a member or the owner." } + "isMember": { "type": "boolean" } } } }, diff --git a/src/core/community/acl.ts b/src/core/community/acl.ts new file mode 100644 index 0000000..b043f30 --- /dev/null +++ b/src/core/community/acl.ts @@ -0,0 +1,107 @@ +import type { AccessLevel } from "./types"; +import { ACCESS_LEVELS, rankOf } from "./types"; +import type { CommunityAdapter } from "./adapter"; + +export interface EffectiveLevelOptions { + /** Hard cap on recursion depth when walking group-of-groups. */ + maxDepth?: number; +} + +const DEFAULT_MAX_DEPTH = 8; + +/** Resolve the effective access level a DID has in `spaceUri`. + * + * - Direct grants: rows where subject = did. + * - Indirect grants: rows where subject is another space the DID is + * (transitively) a member of. The path-level is the MIN of every level + * traversed, since delegation caps access at each hop. + * + * Returns `null` if the caller has no resolvable access. */ +export async function resolveEffectiveLevel( + adapter: CommunityAdapter, + spaceUri: string, + callerDid: string, + opts: EffectiveLevelOptions = {} +): Promise { + const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH; + // Walk: start with spaceUri. At each step we see who is a direct member; + // if the caller is present, record their level (capped by the path minimum). + let best: number = -1; + const visited = new Set(); + + async function walk(targetSpace: string, pathMin: number, depth: number): Promise { + if (depth > maxDepth) return; + if (visited.has(targetSpace)) return; + visited.add(targetSpace); + const rows = await adapter.listAccessRows(targetSpace); + for (const row of rows) { + const levelRank = rankOf(row.accessLevel); + const capped = pathMin < 0 ? levelRank : Math.min(pathMin, levelRank); + if (row.subjectDid === callerDid) { + if (capped > best) best = capped; + } else if (row.subjectSpaceUri) { + // Walk into the delegated space with the capped path level. + await walk(row.subjectSpaceUri, capped, depth + 1); + } + } + } + + await walk(spaceUri, -1, 0); + return best >= 0 ? ACCESS_LEVELS[best]! : null; +} + +/** Flatten the effective membership (DIDs with level ≥ `member`) for a space. + * Used by the reconciler to write `spaces_members`. */ +export async function flattenEffectiveMembers( + adapter: CommunityAdapter, + spaceUri: string, + opts: EffectiveLevelOptions = {} +): Promise> { + const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH; + const dids = new Set(); + const visited = new Set(); + + async function walk(targetSpace: string, depth: number): Promise { + if (depth > maxDepth) return; + if (visited.has(targetSpace)) return; + visited.add(targetSpace); + const rows = await adapter.listAccessRows(targetSpace); + for (const row of rows) { + if (row.subjectDid) { + dids.add(row.subjectDid); + } else if (row.subjectSpaceUri) { + await walk(row.subjectSpaceUri, depth + 1); + } + } + } + + await walk(spaceUri, 0); + return dids; +} + +/** Check whether the actor would cause a cycle if added as a subject-space of `spaceUri`. */ +export async function wouldCycle( + adapter: CommunityAdapter, + spaceUri: string, + subjectSpaceUri: string +): Promise { + // A cycle exists if spaceUri is reachable from subjectSpaceUri via the + // subject-space graph. + const visited = new Set(); + const stack: string[] = [subjectSpaceUri]; + while (stack.length) { + const s = stack.pop()!; + if (s === spaceUri) return true; + if (visited.has(s)) continue; + visited.add(s); + const rows = await adapter.listAccessRows(s); + for (const row of rows) { + if (row.subjectSpaceUri && !visited.has(row.subjectSpaceUri)) { + stack.push(row.subjectSpaceUri); + } + } + } + return false; +} + +export { ACCESS_LEVELS, rankOf }; diff --git a/src/core/community/adapter.ts b/src/core/community/adapter.ts new file mode 100644 index 0000000..8ef88c3 --- /dev/null +++ b/src/core/community/adapter.ts @@ -0,0 +1,302 @@ +import type { Database } from "../types"; +import type { AccessLevel, AccessLevelRow, CommunityMode, CommunityRow } from "./types"; + +function toNum(v: unknown): number { + return typeof v === "string" ? Number(v) : (v as number); +} + +function mapCommunityRow(row: any): CommunityRow { + return { + did: row.did, + mode: row.mode as CommunityMode, + pdsEndpoint: row.pds_endpoint ?? null, + appPasswordEncrypted: row.app_password_encrypted + ? toBytes(row.app_password_encrypted) + : null, + identifier: row.identifier ?? null, + signingKeyEncrypted: row.signing_key_encrypted + ? toBytes(row.signing_key_encrypted) + : null, + rotationKeyEncrypted: row.rotation_key_encrypted + ? toBytes(row.rotation_key_encrypted) + : null, + createdBy: row.created_by, + createdAt: toNum(row.created_at), + deletedAt: row.deleted_at == null ? null : toNum(row.deleted_at), + }; +} + +function toBytes(v: unknown): Uint8Array { + // Encrypted fields are stored base64 as TEXT; passthrough for other code paths. + if (v instanceof Uint8Array) return v; + if (typeof v === "string") { + // Treat as base64 for storage. Caller's cipher handles further decoding. + return new TextEncoder().encode(v); + } + throw new Error("unexpected blob column type"); +} + +function mapAccessRow(row: any): AccessLevelRow { + const isSpace = row.subject_kind === "space"; + return { + spaceUri: row.space_uri, + subjectDid: isSpace ? null : row.subject, + subjectSpaceUri: isSpace ? row.subject : null, + accessLevel: row.access_level as AccessLevel, + grantedBy: row.granted_by, + grantedAt: toNum(row.granted_at), + }; +} + +export interface CreateAdoptedCommunityInput { + did: string; + pdsEndpoint: string; + appPasswordEncrypted: string; // base64 envelope + identifier: string; + createdBy: string; +} + +export interface CreateMintedCommunityInput { + did: string; + signingKeyEncrypted: string; // base64 envelope + rotationKeyEncrypted: string; // base64 envelope + createdBy: string; +} + +export interface GrantInput { + spaceUri: string; + subjectDid?: string; + subjectSpaceUri?: string; + accessLevel: AccessLevel; + grantedBy: string; +} + +export class CommunityAdapter { + constructor(private readonly db: Database) {} + + // ---- Communities ------------------------------------------------------- + + async createAdoptedCommunity(input: CreateAdoptedCommunityInput): Promise { + const now = Date.now(); + await this.db + .prepare( + `INSERT INTO communities (did, mode, pds_endpoint, app_password_encrypted, identifier, created_by, created_at) + VALUES (?, 'adopt', ?, ?, ?, ?, ?)` + ) + .bind( + input.did, + input.pdsEndpoint, + input.appPasswordEncrypted, + input.identifier, + input.createdBy, + now + ) + .run(); + return { + did: input.did, + mode: "adopt", + pdsEndpoint: input.pdsEndpoint, + appPasswordEncrypted: new TextEncoder().encode(input.appPasswordEncrypted), + identifier: input.identifier, + signingKeyEncrypted: null, + rotationKeyEncrypted: null, + createdBy: input.createdBy, + createdAt: now, + deletedAt: null, + }; + } + + async createMintedCommunity(input: CreateMintedCommunityInput): Promise { + const now = Date.now(); + await this.db + .prepare( + `INSERT INTO communities (did, mode, signing_key_encrypted, rotation_key_encrypted, created_by, created_at) + VALUES (?, 'mint', ?, ?, ?, ?)` + ) + .bind( + input.did, + input.signingKeyEncrypted, + input.rotationKeyEncrypted, + input.createdBy, + now + ) + .run(); + return { + did: input.did, + mode: "mint", + pdsEndpoint: null, + appPasswordEncrypted: null, + identifier: null, + signingKeyEncrypted: new TextEncoder().encode(input.signingKeyEncrypted), + rotationKeyEncrypted: new TextEncoder().encode(input.rotationKeyEncrypted), + createdBy: input.createdBy, + createdAt: now, + deletedAt: null, + }; + } + + async getCommunity(did: string): Promise { + const row = await this.db + .prepare(`SELECT * FROM communities WHERE did = ? AND deleted_at IS NULL`) + .bind(did) + .first(); + return row ? mapCommunityRow(row) : null; + } + + /** Look up the raw encrypted credential strings for the community. */ + async getRawCredentials(did: string): Promise<{ + pdsEndpoint: string | null; + appPasswordEncrypted: string | null; + signingKeyEncrypted: string | null; + rotationKeyEncrypted: string | null; + identifier: string | null; + mode: CommunityMode; + } | null> { + const row = await this.db + .prepare( + `SELECT mode, pds_endpoint, app_password_encrypted, signing_key_encrypted, + rotation_key_encrypted, identifier + FROM communities WHERE did = ? AND deleted_at IS NULL` + ) + .bind(did) + .first(); + if (!row) return null; + return { + mode: row.mode as CommunityMode, + pdsEndpoint: row.pds_endpoint ?? null, + appPasswordEncrypted: row.app_password_encrypted ?? null, + signingKeyEncrypted: row.signing_key_encrypted ?? null, + rotationKeyEncrypted: row.rotation_key_encrypted ?? null, + identifier: row.identifier ?? null, + }; + } + + async updateAdoptedCredentials(input: { + did: string; + pdsEndpoint: string; + appPasswordEncrypted: string; + identifier: string; + }): Promise { + await this.db + .prepare( + `UPDATE communities + SET pds_endpoint = ?, app_password_encrypted = ?, identifier = ? + WHERE did = ? AND mode = 'adopt'` + ) + .bind(input.pdsEndpoint, input.appPasswordEncrypted, input.identifier, input.did) + .run(); + } + + async listCommunitiesForActor(actor: string): Promise { + // Communities where the actor has any access level in any community-owned space. + const { results } = await this.db + .prepare( + `SELECT DISTINCT c.* FROM communities c + JOIN spaces s ON s.owner_did = c.did AND s.deleted_at IS NULL + JOIN community_access_levels cal + ON cal.space_uri = s.uri + AND cal.subject_kind = 'did' AND cal.subject = ? + WHERE c.deleted_at IS NULL + ORDER BY c.created_at DESC` + ) + .bind(actor) + .all(); + return results.map(mapCommunityRow); + } + + async softDeleteCommunity(did: string): Promise { + const now = Date.now(); + await this.db + .prepare(`UPDATE communities SET deleted_at = ? WHERE did = ?`) + .bind(now, did) + .run(); + // Cascade soft-delete all spaces owned by the community. + await this.db + .prepare( + `UPDATE spaces SET deleted_at = ? WHERE owner_did = ? AND deleted_at IS NULL` + ) + .bind(now, did) + .run(); + } + + // ---- Access levels ----------------------------------------------------- + + async grant(input: GrantInput): Promise { + if ((input.subjectDid ? 1 : 0) + (input.subjectSpaceUri ? 1 : 0) !== 1) { + throw new Error("grant requires exactly one of subjectDid or subjectSpaceUri"); + } + const kind = input.subjectDid ? "did" : "space"; + const subject = (input.subjectDid ?? input.subjectSpaceUri)!; + const now = Date.now(); + await this.db + .prepare( + `INSERT INTO community_access_levels (space_uri, subject, subject_kind, access_level, granted_by, granted_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (space_uri, subject) DO UPDATE SET + access_level = excluded.access_level, + granted_by = excluded.granted_by, + granted_at = excluded.granted_at` + ) + .bind(input.spaceUri, subject, kind, input.accessLevel, input.grantedBy, now) + .run(); + } + + async revoke(input: { + spaceUri: string; + subjectDid?: string; + subjectSpaceUri?: string; + }): Promise { + if ((input.subjectDid ? 1 : 0) + (input.subjectSpaceUri ? 1 : 0) !== 1) { + throw new Error("revoke requires exactly one of subjectDid or subjectSpaceUri"); + } + const subject = (input.subjectDid ?? input.subjectSpaceUri)!; + await this.db + .prepare( + `DELETE FROM community_access_levels WHERE space_uri = ? AND subject = ?` + ) + .bind(input.spaceUri, subject) + .run(); + } + + async getAccessRow( + spaceUri: string, + subject: string + ): Promise { + const row = await this.db + .prepare( + `SELECT * FROM community_access_levels WHERE space_uri = ? AND subject = ?` + ) + .bind(spaceUri, subject) + .first(); + return row ? mapAccessRow(row) : null; + } + + async listAccessRows(spaceUri: string): Promise { + const { results } = await this.db + .prepare( + `SELECT * FROM community_access_levels WHERE space_uri = ? ORDER BY granted_at ASC` + ) + .bind(spaceUri) + .all(); + return results.map(mapAccessRow); + } + + /** Spaces that reference `subjectSpaceUri` as a member — used for reverse-graph reconciliation. */ + async listSpacesDelegatingTo(subjectSpaceUri: string): Promise { + const { results } = await this.db + .prepare( + `SELECT DISTINCT space_uri FROM community_access_levels + WHERE subject_kind = 'space' AND subject = ?` + ) + .bind(subjectSpaceUri) + .all(); + return results.map((r: any) => r.space_uri); + } + + async deleteAllAccessForSpace(spaceUri: string): Promise { + await this.db + .prepare(`DELETE FROM community_access_levels WHERE space_uri = ?`) + .bind(spaceUri) + .run(); + } +} diff --git a/src/core/community/credentials.ts b/src/core/community/credentials.ts new file mode 100644 index 0000000..3acb5c2 --- /dev/null +++ b/src/core/community/credentials.ts @@ -0,0 +1,117 @@ +/** Envelope-encryption helpers for community credentials. + * AES-GCM with a 32-byte master key provided via config. */ + +const IV_LEN = 12; + +function normalizeKey(key: Uint8Array | string): Uint8Array { + if (typeof key !== "string") { + if (key.length !== 32) { + throw new Error(`community master key must be 32 bytes, got ${key.length}`); + } + return key; + } + // Try base64 first, fall back to hex. + const bytes = tryBase64(key) ?? tryHex(key); + if (!bytes) { + throw new Error( + "community master key must be a 32-byte Uint8Array or base64/hex string" + ); + } + if (bytes.length !== 32) { + throw new Error(`community master key must decode to 32 bytes, got ${bytes.length}`); + } + return bytes; +} + +function tryBase64(s: string): Uint8Array | null { + try { + const normal = s.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normal + "=".repeat((4 - (normal.length % 4)) % 4); + if (!/^[A-Za-z0-9+/]*=*$/.test(padded)) return null; + const bin = atob(padded); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; + } catch { + return null; + } +} + +function tryHex(s: string): Uint8Array | null { + if (!/^[0-9a-fA-F]+$/.test(s) || s.length % 2 !== 0) return null; + const out = new Uint8Array(s.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16); + } + return out; +} + +function bytesToBase64(bytes: Uint8Array): string { + let bin = ""; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin); +} + +function base64ToBytes(s: string): Uint8Array { + const bin = atob(s); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +async function importKey(raw: Uint8Array): Promise { + return crypto.subtle.importKey( + "raw", + raw as BufferSource, + { name: "AES-GCM" }, + false, + ["encrypt", "decrypt"] + ); +} + +export class CredentialCipher { + private readonly keyPromise: Promise; + + constructor(masterKey: Uint8Array | string) { + this.keyPromise = importKey(normalizeKey(masterKey)); + } + + /** Encrypts `plaintext` and returns a base64 string containing iv || ciphertext. */ + async encrypt(plaintext: string | Uint8Array): Promise { + const key = await this.keyPromise; + const iv = crypto.getRandomValues(new Uint8Array(IV_LEN)); + const bytes = + typeof plaintext === "string" + ? new TextEncoder().encode(plaintext) + : plaintext; + const ct = await crypto.subtle.encrypt( + { name: "AES-GCM", iv: iv as BufferSource }, + key, + bytes as BufferSource + ); + const combined = new Uint8Array(iv.length + ct.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(ct), iv.length); + return bytesToBase64(combined); + } + + async decrypt(encoded: string): Promise { + const key = await this.keyPromise; + const combined = base64ToBytes(encoded); + if (combined.length <= IV_LEN) { + throw new Error("ciphertext too short"); + } + const iv = combined.subarray(0, IV_LEN); + const ct = combined.subarray(IV_LEN); + const pt = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv as BufferSource }, + key, + ct as BufferSource + ); + return new Uint8Array(pt); + } + + async decryptString(encoded: string): Promise { + return new TextDecoder().decode(await this.decrypt(encoded)); + } +} diff --git a/src/core/community/index.ts b/src/core/community/index.ts new file mode 100644 index 0000000..5d3852e --- /dev/null +++ b/src/core/community/index.ts @@ -0,0 +1,33 @@ +export { registerCommunityRoutes } from "./router"; +export type { CommunityRoutesOptions } from "./router"; +export { CommunityAdapter } from "./adapter"; +export type { + AccessLevel, + AccessLevelRow, + CommunityConfig, + CommunityMode, + CommunityRow, + ReservedKey, +} from "./types"; +export { + ACCESS_LEVELS, + RESERVED_KEYS, + isAccessLevel, + isReservedKey, + rankOf, +} from "./types"; +export { CredentialCipher } from "./credentials"; +export { resolveEffectiveLevel, flattenEffectiveMembers, wouldCycle } from "./acl"; +export { reconcile } from "./reconcile"; +export { initCommunitySchema, buildCommunitySchema } from "./schema"; +export { resolveIdentity, createPdsSession } from "./pds"; +export { + generateKeyPair, + buildGenesisOp, + signGenesisOp, + computeDidPlc, + submitGenesisOp, + encodeDagCbor, + jwkToDidKey, +} from "./plc"; +export type { KeyPair, GenesisOpInput, UnsignedGenesisOp, SignedGenesisOp } from "./plc"; diff --git a/src/core/community/pds.ts b/src/core/community/pds.ts new file mode 100644 index 0000000..2f73181 --- /dev/null +++ b/src/core/community/pds.ts @@ -0,0 +1,137 @@ +/** Helpers for interacting with a community's PDS account — resolving identity + * and creating sessions from stored app passwords. */ + +import { + CompositeDidDocumentResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + type DidDocumentResolver, +} from "@atcute/identity-resolver"; + +export interface ResolvedIdentity { + did: string; + handle: string | null; + pdsEndpoint: string; +} + +export interface PdsSession { + accessJwt: string; + refreshJwt: string; + did: string; +} + +function defaultResolver(): DidDocumentResolver { + return new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver(), + }, + }) as unknown as DidDocumentResolver; +} + +/** Given a handle or DID, resolve to { did, pdsEndpoint } using the DID doc's + * `atproto_pds` service entry. */ +export async function resolveIdentity( + identifier: string, + opts: { resolver?: DidDocumentResolver; fetch?: typeof fetch } = {} +): Promise { + const f = opts.fetch ?? fetch; + let did = identifier; + let handle: string | null = null; + + if (!identifier.startsWith("did:")) { + // Handle → DID via /.well-known or _atproto DNS. + did = await resolveHandleToDid(identifier, f); + handle = identifier; + } + + const resolver = opts.resolver ?? defaultResolver(); + const doc = await (resolver as any).resolve(did); + if (!doc) throw new Error(`could not resolve DID document for ${did}`); + + const services: Array<{ id?: string; type?: string; serviceEndpoint?: string }> = + (doc as any).service ?? []; + const pds = services.find( + (s) => + s.type === "AtprotoPersonalDataServer" || + s.id === "#atproto_pds" || + s.id?.endsWith("#atproto_pds") + ); + if (!pds?.serviceEndpoint) { + throw new Error(`DID document for ${did} has no atproto_pds service`); + } + + return { did, handle, pdsEndpoint: pds.serviceEndpoint }; +} + +async function resolveHandleToDid(handle: string, f: typeof fetch): Promise { + // Try /.well-known/atproto-did first (cheaper, no DNS). + try { + const res = await f(`https://${handle}/.well-known/atproto-did`, { + redirect: "follow", + }); + if (res.ok) { + const did = (await res.text()).trim(); + if (did.startsWith("did:")) return did; + } + } catch { + /* fall through */ + } + // Fallback: DNS TXT _atproto.. Not available in Workers without + // a DNS-over-HTTPS provider; use Cloudflare's 1.1.1.1 as a default. + try { + const res = await f( + `https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`, + { headers: { accept: "application/dns-json" } } + ); + if (res.ok) { + const body = (await res.json()) as { + Answer?: Array<{ data?: string; type?: number }>; + }; + for (const ans of body.Answer ?? []) { + if (ans.type === 16 && ans.data) { + const trimmed = ans.data.replace(/^"|"$/g, ""); + const m = /^did=(did:[^"\s]+)$/.exec(trimmed); + if (m) return m[1]!; + } + } + } + } catch { + /* fall through */ + } + throw new Error(`could not resolve handle ${handle}`); +} + +/** Create an atproto session on the given PDS using identifier + app password. + * Returns the access/refresh JWTs and the session's DID. */ +export async function createPdsSession( + pdsEndpoint: string, + identifier: string, + appPassword: string, + opts: { fetch?: typeof fetch } = {} +): Promise { + const f = opts.fetch ?? fetch; + const url = `${pdsEndpoint.replace(/\/$/, "")}/xrpc/com.atproto.server.createSession`; + const res = await f(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ identifier, password: appPassword }), + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`createSession failed (${res.status}): ${text}`); + } + const body = (await res.json()) as { + accessJwt?: string; + refreshJwt?: string; + did?: string; + }; + if (!body.accessJwt || !body.refreshJwt || !body.did) { + throw new Error("createSession response missing expected fields"); + } + return { + accessJwt: body.accessJwt, + refreshJwt: body.refreshJwt, + did: body.did, + }; +} diff --git a/src/core/community/plc.ts b/src/core/community/plc.ts new file mode 100644 index 0000000..b5dcc1c --- /dev/null +++ b/src/core/community/plc.ts @@ -0,0 +1,312 @@ +/** did:plc minting: key generation, genesis op construction, signing, + * submission. Zero external deps — uses Web Crypto for P-256 and a + * minimal hand-rolled DAG-CBOR encoder for the specific op shape. */ + +// ============================================================================ +// Key handling (P-256 / ES256) +// ============================================================================ + +export interface KeyPair { + /** JWK of the private key; stored encrypted. */ + privateJwk: JsonWebKey; + /** did:key multibase encoding of the public key. */ + publicDidKey: string; +} + +export async function generateKeyPair(): Promise { + const pair = (await crypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign", "verify"] + )) as CryptoKeyPair; + const privateJwk = (await crypto.subtle.exportKey("jwk", pair.privateKey)) as JsonWebKey; + const publicJwk = (await crypto.subtle.exportKey("jwk", pair.publicKey)) as JsonWebKey; + return { privateJwk, publicDidKey: jwkToDidKey(publicJwk) }; +} + +/** Convert a P-256 JWK public key to did:key format. + * multicodec for P-256 pub: 0x1200 (varint: [0x80, 0x24]). + * did:key:zBase58btc(multicodec || compressed-pub-key). */ +export function jwkToDidKey(jwk: JsonWebKey): string { + if (jwk.kty !== "EC" || jwk.crv !== "P-256" || !jwk.x || !jwk.y) { + throw new Error("expected EC P-256 JWK"); + } + const x = b64urlToBytes(jwk.x); + const y = b64urlToBytes(jwk.y); + if (x.length !== 32 || y.length !== 32) { + throw new Error("malformed P-256 JWK"); + } + // Compressed form: 0x02 if y is even, 0x03 if odd, then x. + const prefix = (y[31]! & 1) === 0 ? 0x02 : 0x03; + const compressed = new Uint8Array(33); + compressed[0] = prefix; + compressed.set(x, 1); + const multicodec = new Uint8Array([0x80, 0x24]); + const combined = new Uint8Array(multicodec.length + compressed.length); + combined.set(multicodec, 0); + combined.set(compressed, multicodec.length); + return "did:key:z" + base58btcEncode(combined); +} + +async function signBytes(privateJwk: JsonWebKey, bytes: Uint8Array): Promise { + const key = await crypto.subtle.importKey( + "jwk", + privateJwk, + { name: "ECDSA", namedCurve: "P-256" }, + false, + ["sign"] + ); + const sig = await crypto.subtle.sign( + { name: "ECDSA", hash: "SHA-256" }, + key, + bytes as BufferSource + ); + return new Uint8Array(sig); +} + +// ============================================================================ +// Genesis op construction +// ============================================================================ + +export interface GenesisOpInput { + rotationKeys: string[]; // did:key strings + verificationMethodAtproto: string; // did:key + services?: Record; + alsoKnownAs?: string[]; +} + +export interface UnsignedGenesisOp { + type: "plc_operation"; + rotationKeys: string[]; + verificationMethods: Record; + alsoKnownAs: string[]; + services: Record; + prev: null; +} + +export interface SignedGenesisOp extends UnsignedGenesisOp { + sig: string; // base64url, unpadded +} + +export function buildGenesisOp(input: GenesisOpInput): UnsignedGenesisOp { + return { + type: "plc_operation", + rotationKeys: input.rotationKeys, + verificationMethods: { atproto: input.verificationMethodAtproto }, + alsoKnownAs: input.alsoKnownAs ?? [], + services: input.services ?? {}, + prev: null, + }; +} + +/** Sign a genesis op with a rotation key's private JWK. */ +export async function signGenesisOp( + op: UnsignedGenesisOp, + signerPrivateJwk: JsonWebKey +): Promise { + const encoded = encodeDagCbor(op); + const sigBytes = await signBytes(signerPrivateJwk, encoded); + return { ...op, sig: bytesToB64url(sigBytes) }; +} + +/** Compute the did:plc from a signed genesis op. + * The DID is `did:plc:` + base32-lower-unpadded(sha256(cbor(signedOp)))[:24]. */ +export async function computeDidPlc(signedOp: SignedGenesisOp): Promise { + const encoded = encodeDagCbor(signedOp); + const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoded as BufferSource)); + return "did:plc:" + base32Lower(hash).slice(0, 24); +} + +/** Submit a signed genesis op to the PLC directory. */ +export async function submitGenesisOp( + plcDirectory: string, + did: string, + signedOp: SignedGenesisOp, + opts: { fetch?: typeof fetch } = {} +): Promise { + const f = opts.fetch ?? fetch; + const url = `${plcDirectory.replace(/\/$/, "")}/${did}`; + const res = await f(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(signedOp), + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`PLC submit failed (${res.status}): ${text}`); + } +} + +// ============================================================================ +// Minimal DAG-CBOR encoder +// Only supports the types needed for PLC genesis ops: +// null, text strings, arrays, maps with string keys. +// Maps are canonicalized: keys sorted by their CBOR byte encoding, +// ascending lexicographically. Integers use the smallest encoding. +// See RFC 8949 + https://ipld.io/specs/codecs/dag-cbor/spec/ +// ============================================================================ + +export function encodeDagCbor(value: unknown): Uint8Array { + const chunks: Uint8Array[] = []; + encode(value, chunks); + return concat(chunks); +} + +function encode(value: unknown, out: Uint8Array[]): void { + if (value === null) { + out.push(new Uint8Array([0xf6])); + return; + } + if (value === false) { + out.push(new Uint8Array([0xf4])); + return; + } + if (value === true) { + out.push(new Uint8Array([0xf5])); + return; + } + if (typeof value === "string") { + const bytes = new TextEncoder().encode(value); + encodeHead(3, bytes.length, out); + out.push(bytes); + return; + } + if (typeof value === "number") { + if (!Number.isInteger(value)) { + throw new Error("DAG-CBOR: floats not supported in PLC op encoder"); + } + if (value >= 0) { + encodeHead(0, value, out); + } else { + encodeHead(1, -value - 1, out); + } + return; + } + if (Array.isArray(value)) { + encodeHead(4, value.length, out); + for (const v of value) encode(v, out); + return; + } + if (typeof value === "object") { + // Map with string keys, canonicalized. + const entries = Object.entries(value as Record).filter( + ([, v]) => v !== undefined + ); + // Sort keys by their CBOR-encoded byte form. + // For DAG-CBOR with text-string keys, this equals: + // (a) shorter UTF-8 byte length first, (b) lexicographic byte order within same length. + entries.sort(([a], [b]) => { + const ab = new TextEncoder().encode(a); + const bb = new TextEncoder().encode(b); + if (ab.length !== bb.length) return ab.length - bb.length; + for (let i = 0; i < ab.length; i++) { + if (ab[i] !== bb[i]) return ab[i]! - bb[i]!; + } + return 0; + }); + encodeHead(5, entries.length, out); + for (const [k, v] of entries) { + encode(k, out); + encode(v, out); + } + return; + } + throw new Error(`DAG-CBOR: unsupported value type ${typeof value}`); +} + +function encodeHead(majorType: number, n: number, out: Uint8Array[]): void { + const mt = majorType << 5; + if (n < 24) { + out.push(new Uint8Array([mt | n])); + } else if (n < 0x100) { + out.push(new Uint8Array([mt | 24, n])); + } else if (n < 0x10000) { + out.push(new Uint8Array([mt | 25, (n >> 8) & 0xff, n & 0xff])); + } else if (n < 0x100000000) { + const b = new Uint8Array(5); + b[0] = mt | 26; + b[1] = (n >>> 24) & 0xff; + b[2] = (n >>> 16) & 0xff; + b[3] = (n >>> 8) & 0xff; + b[4] = n & 0xff; + out.push(b); + } else { + throw new Error("DAG-CBOR: integer too large"); + } +} + +function concat(parts: Uint8Array[]): Uint8Array { + let total = 0; + for (const p of parts) total += p.length; + const out = new Uint8Array(total); + let off = 0; + for (const p of parts) { + out.set(p, off); + off += p.length; + } + return out; +} + +// ============================================================================ +// Base encoding helpers (base58btc, base32-lower-unpadded, base64url) +// ============================================================================ + +const B58_ALPHA = + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +function base58btcEncode(bytes: Uint8Array): string { + if (bytes.length === 0) return ""; + // Count leading zeros. + let zeros = 0; + while (zeros < bytes.length && bytes[zeros] === 0) zeros++; + // Repeated divmod by 58 using big-endian byte buffer. + const input = Array.from(bytes); + let encoded = ""; + let start = zeros; + while (start < input.length) { + let carry = 0; + for (let i = start; i < input.length; i++) { + const v = (carry << 8) + input[i]!; + input[i] = Math.floor(v / 58); + carry = v % 58; + } + encoded = B58_ALPHA[carry]! + encoded; + while (start < input.length && input[start] === 0) start++; + } + return "1".repeat(zeros) + encoded; +} + +const B32_ALPHA = "abcdefghijklmnopqrstuvwxyz234567"; + +function base32Lower(bytes: Uint8Array): string { + let bits = 0; + let value = 0; + let out = ""; + for (let i = 0; i < bytes.length; i++) { + value = (value << 8) | bytes[i]!; + bits += 8; + while (bits >= 5) { + bits -= 5; + out += B32_ALPHA[(value >>> bits) & 31]; + } + } + if (bits > 0) { + out += B32_ALPHA[(value << (5 - bits)) & 31]; + } + return out; +} + +function bytesToB64url(bytes: Uint8Array): string { + let bin = ""; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!); + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function b64urlToBytes(s: string): Uint8Array { + const normal = s.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normal + "=".repeat((4 - (normal.length % 4)) % 4); + const bin = atob(padded); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} diff --git a/src/core/community/reconcile.ts b/src/core/community/reconcile.ts new file mode 100644 index 0000000..b9d7e35 --- /dev/null +++ b/src/core/community/reconcile.ts @@ -0,0 +1,42 @@ +import type { StorageAdapter as SpacesAdapter } from "../spaces/types"; +import type { CommunityAdapter } from "./adapter"; +import { flattenEffectiveMembers } from "./acl"; + +/** Reconcile `spaces_members` for `spaceUri` to match the flattened effective + * member set derived from `community_access_levels`. Also re-reconciles any + * spaces that delegate to this one (reverse-graph). */ +export async function reconcile( + community: CommunityAdapter, + spaces: SpacesAdapter, + spaceUri: string, + byDid: string, + opts: { depth?: number; maxReverseDepth?: number } = {} +): Promise { + const maxReverse = opts.maxReverseDepth ?? 8; + const startDepth = opts.depth ?? 0; + if (startDepth > maxReverse) return; + + const effective = await flattenEffectiveMembers(community, spaceUri); + const current = new Set( + (await spaces.listMembers(spaceUri)).map((m) => m.did) + ); + + const adds: string[] = []; + const removes: string[] = []; + for (const did of effective) if (!current.has(did)) adds.push(did); + for (const did of current) if (!effective.has(did)) removes.push(did); + + if (adds.length || removes.length) { + await spaces.applyMembershipDiff(spaceUri, adds, removes, byDid); + } + + // Reverse-graph: if this space is a subject of other spaces' access rows, + // their effective membership may have changed too. + const parents = await community.listSpacesDelegatingTo(spaceUri); + for (const parent of parents) { + await reconcile(community, spaces, parent, byDid, { + depth: startDepth + 1, + maxReverseDepth: maxReverse, + }); + } +} diff --git a/src/core/community/router.ts b/src/core/community/router.ts new file mode 100644 index 0000000..07be342 --- /dev/null +++ b/src/core/community/router.ts @@ -0,0 +1,1008 @@ +import type { Context, Hono, MiddlewareHandler } from "hono"; +import type { ContrailConfig, Database } from "../types"; +import type { ServiceAuth } from "../spaces/auth"; +import type { StorageAdapter as SpacesAdapter } from "../spaces/types"; +import { buildSpaceUri } from "../spaces/uri"; +import { HostedAdapter } from "../spaces/adapter"; +import { CommunityAdapter } from "./adapter"; +import { CredentialCipher } from "./credentials"; +import { resolveIdentity, createPdsSession } from "./pds"; +import { + generateKeyPair, + buildGenesisOp, + signGenesisOp, + computeDidPlc, + submitGenesisOp, +} from "./plc"; +import { resolveEffectiveLevel, wouldCycle } from "./acl"; +import { reconcile } from "./reconcile"; +import type { AccessLevel } from "./types"; +import { + ACCESS_LEVELS, + rankOf, + isAccessLevel, + isReservedKey, + RESERVED_KEYS, +} from "./types"; +import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; + +export interface CommunityRoutesOptions { + /** Override auth middleware for tests. */ + authMiddleware?: MiddlewareHandler; + /** Storage adapter overrides (defaults: HostedAdapter on the given db). */ + communityAdapter?: CommunityAdapter; + spacesAdapter?: SpacesAdapter; +} + +export function registerCommunityRoutes( + app: Hono, + db: Database, + config: ContrailConfig, + options: CommunityRoutesOptions = {}, + ctx?: { + spacesAdapter: SpacesAdapter; + verifier: ServiceJwtVerifier; + } | null +): void { + const cfg = config.community; + if (!cfg) return; + if (!config.spaces) { + throw new Error("community module requires spaces to be enabled in config"); + } + + const community = + options.communityAdapter ?? new CommunityAdapter(db); + const spaces = + options.spacesAdapter ?? ctx?.spacesAdapter ?? new HostedAdapter(db, config); + const cipher = new CredentialCipher(cfg.masterKey); + + const auth = + options.authMiddleware ?? + (() => { + throw new Error( + "community routes require an authMiddleware. Pass options.authMiddleware or ensure spaces.authMiddleware is configured." + ); + })(); + + const NS = `${config.namespace}.community`; + const spaceType = config.spaces.type; + const spaceServiceDid = cfg.serviceDid ?? config.spaces.serviceDid; + + // ========================================================================== + // Community lifecycle + // ========================================================================== + + app.post(`/xrpc/${NS}.adopt`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { identifier?: string; appPassword?: string } + | null; + if (!body?.identifier || !body.appPassword) { + return c.json( + { error: "InvalidRequest", message: "identifier and appPassword required" }, + 400 + ); + } + + // 1. Resolve identifier → DID + PDS endpoint. + let resolved; + try { + resolved = await resolveIdentity(body.identifier, { + resolver: cfg.resolver, + fetch: cfg.fetch, + }); + } catch (err: any) { + return c.json( + { error: "InvalidRequest", message: `could not resolve: ${err.message}` }, + 400 + ); + } + + // 2. Verify the credentials by creating a session. + try { + await createPdsSession( + resolved.pdsEndpoint, + body.identifier, + body.appPassword, + { fetch: cfg.fetch } + ); + } catch (err: any) { + return c.json( + { error: "Unauthorized", message: `credential check failed: ${err.message}` }, + 401 + ); + } + + // 3. Check not already adopted. + const existing = await community.getCommunity(resolved.did); + if (existing) { + return c.json({ error: "AlreadyExists", did: resolved.did }, 409); + } + + // 4. Encrypt + store. + const encrypted = await cipher.encrypt(body.appPassword); + await community.createAdoptedCommunity({ + did: resolved.did, + pdsEndpoint: resolved.pdsEndpoint, + appPasswordEncrypted: encrypted, + identifier: body.identifier, + createdBy: sa.issuer, + }); + + // 5. Bootstrap reserved spaces with caller as owner. + await bootstrapReservedSpaces({ + communityDid: resolved.did, + creatorDid: sa.issuer, + spaces, + community, + type: spaceType, + serviceDid: spaceServiceDid, + }); + + return c.json({ communityDid: resolved.did }); + }); + + app.post(`/xrpc/${NS}.mint`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { handle?: string; pdsEndpoint?: string } + | null; + + // Generate three keypairs: signing (atproto verificationMethod), + // contrail-held rotation, creator-held rotation (recovery). + const signingKey = await generateKeyPair(); + const contrailRotation = await generateKeyPair(); + const creatorRotation = await generateKeyPair(); + + // Build + sign genesis op using contrail's rotation key. + const unsigned = buildGenesisOp({ + rotationKeys: [contrailRotation.publicDidKey, creatorRotation.publicDidKey], + verificationMethodAtproto: signingKey.publicDidKey, + alsoKnownAs: body?.handle ? [`at://${body.handle}`] : [], + services: body?.pdsEndpoint + ? { + atproto_pds: { + type: "AtprotoPersonalDataServer", + endpoint: body.pdsEndpoint, + }, + } + : {}, + }); + const signed = await signGenesisOp(unsigned, contrailRotation.privateJwk); + const did = await computeDidPlc(signed); + + // Submit to PLC directory. + const plcDir = cfg.plcDirectory ?? "https://plc.directory"; + try { + await submitGenesisOp(plcDir, did, signed, { fetch: cfg.fetch }); + } catch (err: any) { + return c.json( + { error: "UpstreamFailure", message: err.message }, + 502 + ); + } + + // Encrypt stored keys (signing + contrail rotation). The creator's + // rotation key is returned once and never stored. + const signingEncrypted = await cipher.encrypt(JSON.stringify(signingKey.privateJwk)); + const contrailRotEncrypted = await cipher.encrypt( + JSON.stringify(contrailRotation.privateJwk) + ); + + await community.createMintedCommunity({ + did, + signingKeyEncrypted: signingEncrypted, + rotationKeyEncrypted: contrailRotEncrypted, + createdBy: sa.issuer, + }); + + await bootstrapReservedSpaces({ + communityDid: did, + creatorDid: sa.issuer, + spaces, + community, + type: spaceType, + serviceDid: spaceServiceDid, + }); + + return c.json({ + communityDid: did, + recoveryKey: creatorRotation.privateJwk, + }); + }); + + app.post(`/xrpc/${NS}.delete`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { communityDid?: string } + | null; + if (!body?.communityDid) { + return c.json({ error: "InvalidRequest", message: "communityDid required" }, 400); + } + const row = await community.getCommunity(body.communityDid); + if (!row) return c.json({ error: "NotFound" }, 404); + + const adminUri = buildSpaceUri({ + ownerDid: body.communityDid, + type: spaceType, + key: "$admin", + }); + const level = await resolveEffectiveLevel(community, adminUri, sa.issuer); + if (level !== "owner") { + return c.json({ error: "Forbidden", reason: "owner-required" }, 403); + } + + await community.softDeleteCommunity(body.communityDid); + return c.json({ ok: true }); + }); + + app.get(`/xrpc/${NS}.list`, auth, async (c) => { + const sa = getAuth(c); + const actor = c.req.query("actor") ?? sa.issuer; + const rows = await community.listCommunitiesForActor(actor); + return c.json({ + communities: rows.map((r) => ({ + did: r.did, + mode: r.mode, + identifier: r.identifier, + createdAt: r.createdAt, + })), + }); + }); + + app.get(`/xrpc/${NS}.whoami`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + if (!spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const level = await resolveEffectiveLevel(community, spaceUri, sa.issuer); + return c.json({ spaceUri, accessLevel: level }); + }); + + // ========================================================================== + // Space lifecycle + // ========================================================================== + + app.post(`/xrpc/${NS}.space.create`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { communityDid?: string; key?: string } + | null; + if (!body?.communityDid) { + return c.json({ error: "InvalidRequest", message: "communityDid required" }, 400); + } + + const communityRow = await community.getCommunity(body.communityDid); + if (!communityRow) return c.json({ error: "NotFound", reason: "community-not-found" }, 404); + + if (body.key && isReservedKey(body.key)) { + return c.json( + { error: "InvalidRequest", reason: "reserved-key", message: `reserved keys cannot be created manually` }, + 400 + ); + } + + // Caller must have admin+ in $admin. + const adminUri = buildSpaceUri({ + ownerDid: body.communityDid, + type: spaceType, + key: "$admin", + }); + const level = await resolveEffectiveLevel(community, adminUri, sa.issuer); + if (!level || rankOf(level) < rankOf("admin")) { + return c.json({ error: "Forbidden", reason: "admin-required-in-admin" }, 403); + } + + const key = body.key ?? generateKey(); + const uri = buildSpaceUri({ ownerDid: body.communityDid, type: spaceType, key }); + + const existing = await spaces.getSpace(uri); + if (existing) return c.json({ error: "AlreadyExists", uri }, 409); + + await spaces.createSpace({ + uri, + ownerDid: body.communityDid, + type: spaceType, + key, + serviceDid: spaceServiceDid, + appPolicyRef: null, + appPolicy: null, + }); + + // Creator becomes owner of the new space. + await community.grant({ + spaceUri: uri, + subjectDid: sa.issuer, + accessLevel: "owner", + grantedBy: sa.issuer, + }); + await reconcile(community, spaces, uri, sa.issuer); + + return c.json({ + space: { + uri, + ownerDid: body.communityDid, + type: spaceType, + key, + serviceDid: spaceServiceDid, + createdAt: Date.now(), + }, + }); + }); + + app.post(`/xrpc/${NS}.space.delete`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as { spaceUri?: string } | null; + if (!body?.spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const communityRow = await community.getCommunity(space.ownerDid); + if (!communityRow) { + return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); + } + + // Block deletion of reserved spaces. + if (isReservedKey(space.key)) { + return c.json({ error: "Forbidden", reason: "reserved-space-cannot-be-deleted" }, 403); + } + + // Caller must be owner of this space OR admin+ in $admin. + const onSpace = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + const onAdmin = await resolveEffectiveLevel( + community, + buildSpaceUri({ ownerDid: space.ownerDid, type: spaceType, key: "$admin" }), + sa.issuer + ); + const allowed = + onSpace === "owner" || (onAdmin != null && rankOf(onAdmin) >= rankOf("admin")); + if (!allowed) { + return c.json({ error: "Forbidden", reason: "owner-or-admin-required" }, 403); + } + + await spaces.deleteSpace(body.spaceUri); + await community.deleteAllAccessForSpace(body.spaceUri); + // Drop the materialized membership too. + const members = await spaces.listMembers(body.spaceUri); + if (members.length) { + await spaces.applyMembershipDiff( + body.spaceUri, + [], + members.map((m) => m.did), + sa.issuer + ); + } + return c.json({ ok: true }); + }); + + // ========================================================================== + // Membership + // ========================================================================== + + app.post(`/xrpc/${NS}.space.grant`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + spaceUri?: string; + subject?: { did?: string; spaceUri?: string }; + accessLevel?: string; + } + | null; + if (!body?.spaceUri || !body.subject || !body.accessLevel) { + return c.json( + { error: "InvalidRequest", message: "spaceUri, subject, accessLevel required" }, + 400 + ); + } + if (!isAccessLevel(body.accessLevel)) { + return c.json( + { error: "InvalidRequest", message: `invalid accessLevel; one of ${ACCESS_LEVELS.join(", ")}` }, + 400 + ); + } + const subjectDid = body.subject.did; + const subjectSpaceUri = body.subject.spaceUri; + if ((subjectDid ? 1 : 0) + (subjectSpaceUri ? 1 : 0) !== 1) { + return c.json( + { error: "InvalidRequest", message: "subject must have exactly one of did or spaceUri" }, + 400 + ); + } + + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const communityRow = await community.getCommunity(space.ownerDid); + if (!communityRow) { + return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); + } + + // Caller must have at least manager. + const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + // Cannot grant higher than own level. + if (rankOf(body.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); + } + + // Cycle check when delegating to another space. + if (subjectSpaceUri) { + if (subjectSpaceUri === body.spaceUri) { + return c.json({ error: "InvalidRequest", reason: "self-reference" }, 400); + } + if (await wouldCycle(community, body.spaceUri, subjectSpaceUri)) { + return c.json({ error: "InvalidRequest", reason: "cycle-detected" }, 400); + } + } + + await community.grant({ + spaceUri: body.spaceUri, + subjectDid, + subjectSpaceUri, + accessLevel: body.accessLevel, + grantedBy: sa.issuer, + }); + await reconcile(community, spaces, body.spaceUri, sa.issuer); + + return c.json({ ok: true }); + }); + + app.post(`/xrpc/${NS}.space.revoke`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + spaceUri?: string; + subject?: { did?: string; spaceUri?: string }; + } + | null; + if (!body?.spaceUri || !body.subject) { + return c.json( + { error: "InvalidRequest", message: "spaceUri and subject required" }, + 400 + ); + } + const subjectDid = body.subject.did; + const subjectSpaceUri = body.subject.spaceUri; + if ((subjectDid ? 1 : 0) + (subjectSpaceUri ? 1 : 0) !== 1) { + return c.json( + { error: "InvalidRequest", message: "subject must have exactly one of did or spaceUri" }, + 400 + ); + } + + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + // Caller must outrank the target's current level. + const subject = subjectDid ?? subjectSpaceUri!; + const existing = await community.getAccessRow(body.spaceUri, subject); + if (!existing) { + return c.json({ error: "NotFound", reason: "no-such-grant" }, 404); + } + const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + if (rankOf(existing.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-revoke-higher-than-self" }, 403); + } + + await community.revoke({ spaceUri: body.spaceUri, subjectDid, subjectSpaceUri }); + await reconcile(community, spaces, body.spaceUri, sa.issuer); + + return c.json({ ok: true }); + }); + + app.get(`/xrpc/${NS}.space.listMembers`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + if (!spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const flatten = c.req.query("flatten") === "true"; + const space = await spaces.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + // Caller must have at least member. + const callerLevel = await resolveEffectiveLevel(community, spaceUri, sa.issuer); + if (!callerLevel) { + return c.json({ error: "Forbidden", reason: "not-member" }, 403); + } + + if (flatten) { + const members = await spaces.listMembers(spaceUri); + return c.json({ members: members.map((m) => ({ did: m.did, addedAt: m.addedAt })) }); + } + + const rows = await community.listAccessRows(spaceUri); + return c.json({ + rows: rows.map((r) => ({ + subject: r.subjectDid + ? { did: r.subjectDid } + : { spaceUri: r.subjectSpaceUri }, + accessLevel: r.accessLevel, + grantedBy: r.grantedBy, + grantedAt: r.grantedAt, + })), + }); + }); + + app.post(`/xrpc/${NS}.space.setAccessLevel`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + spaceUri?: string; + subject?: { did?: string; spaceUri?: string }; + accessLevel?: string; + } + | null; + if (!body?.spaceUri || !body.subject || !body.accessLevel) { + return c.json( + { error: "InvalidRequest", message: "spaceUri, subject, accessLevel required" }, + 400 + ); + } + if (!isAccessLevel(body.accessLevel)) { + return c.json({ error: "InvalidRequest", message: "invalid accessLevel" }, 400); + } + const subjectDid = body.subject.did; + const subjectSpaceUri = body.subject.spaceUri; + if ((subjectDid ? 1 : 0) + (subjectSpaceUri ? 1 : 0) !== 1) { + return c.json( + { error: "InvalidRequest", message: "subject must have exactly one of did or spaceUri" }, + 400 + ); + } + + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const subject = subjectDid ?? subjectSpaceUri!; + const existing = await community.getAccessRow(body.spaceUri, subject); + if (!existing) return c.json({ error: "NotFound", reason: "no-such-grant" }, 404); + + const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + // Caller must outrank both the old level AND the new one. + if (rankOf(existing.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-modify-higher-than-self" }, 403); + } + if (rankOf(body.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); + } + + await community.grant({ + spaceUri: body.spaceUri, + subjectDid, + subjectSpaceUri, + accessLevel: body.accessLevel, + grantedBy: sa.issuer, + }); + await reconcile(community, spaces, body.spaceUri, sa.issuer); + return c.json({ ok: true }); + }); + + app.post(`/xrpc/${NS}.space.resync`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as { spaceUri?: string } | null; + if (!body?.spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const level = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!level || rankOf(level) < rankOf("admin")) { + return c.json({ error: "Forbidden", reason: "admin-required" }, 403); + } + + await reconcile(community, spaces, body.spaceUri, sa.issuer); + return c.json({ ok: true }); + }); + + // ========================================================================== + // Publishing — public records (via community PDS) and in-space records + // (authored by the community DID) + // ========================================================================== + + app.post(`/xrpc/${NS}.putRecord`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + communityDid?: string; + collection?: string; + rkey?: string; + record?: Record; + validate?: boolean; + } + | null; + if (!body?.communityDid || !body.collection || !body.record) { + return c.json( + { error: "InvalidRequest", message: "communityDid, collection, record required" }, + 400 + ); + } + + const row = await community.getCommunity(body.communityDid); + if (!row) return c.json({ error: "NotFound" }, 404); + + if (row.mode === "mint") { + return c.json( + { error: "NotSupported", reason: "publishing-not-supported-for-minted-communities" }, + 400 + ); + } + + // Caller must be member+ in $publishers. + const publishersUri = buildSpaceUri({ + ownerDid: body.communityDid, + type: spaceType, + key: "$publishers", + }); + const level = await resolveEffectiveLevel(community, publishersUri, sa.issuer); + if (!level) { + return c.json({ error: "Forbidden", reason: "not-in-publishers" }, 403); + } + + // Decrypt the stored app password and create a session. + const raw = await community.getRawCredentials(body.communityDid); + if (!raw?.appPasswordEncrypted || !raw.pdsEndpoint || !raw.identifier) { + return c.json( + { error: "InvalidState", reason: "missing-credentials" }, + 500 + ); + } + let session; + try { + const appPassword = await cipher.decryptString(raw.appPasswordEncrypted); + session = await createPdsSession(raw.pdsEndpoint, raw.identifier, appPassword, { + fetch: cfg.fetch, + }); + } catch (err: any) { + return c.json( + { error: "UpstreamFailure", reason: "session-creation-failed", message: err.message }, + 502 + ); + } + + // Proxy createRecord. + const f = cfg.fetch ?? fetch; + const res = await f( + `${raw.pdsEndpoint.replace(/\/$/, "")}/xrpc/com.atproto.repo.createRecord`, + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${session.accessJwt}`, + }, + body: JSON.stringify({ + repo: body.communityDid, + collection: body.collection, + rkey: body.rkey, + record: body.record, + validate: body.validate, + }), + } + ); + if (!res.ok) { + const text = await res.text().catch(() => ""); + return c.json( + { error: "UpstreamFailure", message: `createRecord failed (${res.status}): ${text}` }, + 502 + ); + } + const out = (await res.json()) as { uri?: string; cid?: string }; + return c.json({ uri: out.uri, cid: out.cid }); + }); + + app.post(`/xrpc/${NS}.deleteRecord`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { communityDid?: string; collection?: string; rkey?: string } + | null; + if (!body?.communityDid || !body.collection || !body.rkey) { + return c.json( + { error: "InvalidRequest", message: "communityDid, collection, rkey required" }, + 400 + ); + } + const row = await community.getCommunity(body.communityDid); + if (!row) return c.json({ error: "NotFound" }, 404); + if (row.mode === "mint") { + return c.json({ error: "NotSupported" }, 400); + } + + const publishersUri = buildSpaceUri({ + ownerDid: body.communityDid, + type: spaceType, + key: "$publishers", + }); + const level = await resolveEffectiveLevel(community, publishersUri, sa.issuer); + if (!level) { + return c.json({ error: "Forbidden", reason: "not-in-publishers" }, 403); + } + + const raw = await community.getRawCredentials(body.communityDid); + if (!raw?.appPasswordEncrypted || !raw.pdsEndpoint || !raw.identifier) { + return c.json({ error: "InvalidState" }, 500); + } + let session; + try { + const appPassword = await cipher.decryptString(raw.appPasswordEncrypted); + session = await createPdsSession(raw.pdsEndpoint, raw.identifier, appPassword, { + fetch: cfg.fetch, + }); + } catch (err: any) { + return c.json( + { error: "UpstreamFailure", message: err.message }, + 502 + ); + } + + const f = cfg.fetch ?? fetch; + const res = await f( + `${raw.pdsEndpoint.replace(/\/$/, "")}/xrpc/com.atproto.repo.deleteRecord`, + { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${session.accessJwt}`, + }, + body: JSON.stringify({ + repo: body.communityDid, + collection: body.collection, + rkey: body.rkey, + }), + } + ); + if (!res.ok) { + const text = await res.text().catch(() => ""); + return c.json( + { error: "UpstreamFailure", message: `deleteRecord failed (${res.status}): ${text}` }, + 502 + ); + } + return c.json({ ok: true }); + }); + + app.post(`/xrpc/${NS}.space.putRecord`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + spaceUri?: string; + collection?: string; + rkey?: string; + record?: Record; + } + | null; + if (!body?.spaceUri || !body.collection || !body.record) { + return c.json( + { error: "InvalidRequest", message: "spaceUri, collection, record required" }, + 400 + ); + } + + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + const row = await community.getCommunity(space.ownerDid); + if (!row) return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); + + const level = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!level || rankOf(level) < rankOf("admin")) { + return c.json({ error: "Forbidden", reason: "admin-required" }, 403); + } + + const rkey = body.rkey ?? generateKey(); + const now = Date.now(); + await spaces.putRecord({ + spaceUri: body.spaceUri, + collection: body.collection, + authorDid: space.ownerDid, // community DID + rkey, + cid: null, + record: body.record, + createdAt: now, + }); + return c.json({ rkey, authorDid: space.ownerDid, createdAt: now }); + }); + + app.post(`/xrpc/${NS}.space.deleteRecord`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { spaceUri?: string; collection?: string; rkey?: string } + | null; + if (!body?.spaceUri || !body.collection || !body.rkey) { + return c.json( + { error: "InvalidRequest", message: "spaceUri, collection, rkey required" }, + 400 + ); + } + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + const row = await community.getCommunity(space.ownerDid); + if (!row) return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); + + const level = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!level || rankOf(level) < rankOf("admin")) { + return c.json({ error: "Forbidden", reason: "admin-required" }, 403); + } + + await spaces.deleteRecord(body.spaceUri, body.collection, space.ownerDid, body.rkey); + return c.json({ ok: true }); + }); + + // ========================================================================== + // Credential health + reauth + // ========================================================================== + + app.get(`/xrpc/${NS}.getHealth`, auth, async (c) => { + const sa = getAuth(c); + const communityDid = c.req.query("communityDid"); + if (!communityDid) { + return c.json({ error: "InvalidRequest", message: "communityDid required" }, 400); + } + const row = await community.getCommunity(communityDid); + if (!row) return c.json({ error: "NotFound" }, 404); + + // Any member of $admin can ask. + const adminUri = buildSpaceUri({ + ownerDid: communityDid, + type: spaceType, + key: "$admin", + }); + const level = await resolveEffectiveLevel(community, adminUri, sa.issuer); + if (!level) { + return c.json({ error: "Forbidden", reason: "not-member-of-admin" }, 403); + } + + if (row.mode === "mint") { + // Minted communities: we hold the signing key directly; if decryption works, healthy. + try { + const raw = await community.getRawCredentials(communityDid); + if (raw?.signingKeyEncrypted) await cipher.decrypt(raw.signingKeyEncrypted); + return c.json({ status: "healthy" }); + } catch { + return c.json({ status: "expired" }); + } + } + + // Adopted: attempt a session creation. + const raw = await community.getRawCredentials(communityDid); + if (!raw?.appPasswordEncrypted || !raw.pdsEndpoint || !raw.identifier) { + return c.json({ status: "expired" }); + } + try { + const appPassword = await cipher.decryptString(raw.appPasswordEncrypted); + await createPdsSession(raw.pdsEndpoint, raw.identifier, appPassword, { + fetch: cfg.fetch, + }); + return c.json({ status: "healthy" }); + } catch { + return c.json({ status: "expired" }); + } + }); + + app.post(`/xrpc/${NS}.reauth`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { communityDid?: string; appPassword?: string } + | null; + if (!body?.communityDid || !body.appPassword) { + return c.json( + { error: "InvalidRequest", message: "communityDid and appPassword required" }, + 400 + ); + } + const row = await community.getCommunity(body.communityDid); + if (!row) return c.json({ error: "NotFound" }, 404); + if (row.mode !== "adopt") { + return c.json({ error: "NotSupported", reason: "reauth-only-for-adopted" }, 400); + } + + // Caller must have owner in $admin. + const adminUri = buildSpaceUri({ + ownerDid: body.communityDid, + type: spaceType, + key: "$admin", + }); + const level = await resolveEffectiveLevel(community, adminUri, sa.issuer); + if (level !== "owner") { + return c.json({ error: "Forbidden", reason: "owner-required" }, 403); + } + + // Re-resolve in case the PDS moved, and verify the new password. + const identifier = row.identifier ?? body.communityDid; + let resolved; + try { + resolved = await resolveIdentity(identifier, { + resolver: cfg.resolver, + fetch: cfg.fetch, + }); + } catch (err: any) { + return c.json( + { error: "InvalidRequest", message: `could not resolve: ${err.message}` }, + 400 + ); + } + try { + await createPdsSession(resolved.pdsEndpoint, identifier, body.appPassword, { + fetch: cfg.fetch, + }); + } catch (err: any) { + return c.json( + { error: "Unauthorized", message: `credential check failed: ${err.message}` }, + 401 + ); + } + + const encrypted = await cipher.encrypt(body.appPassword); + await community.updateAdoptedCredentials({ + did: body.communityDid, + pdsEndpoint: resolved.pdsEndpoint, + appPasswordEncrypted: encrypted, + identifier, + }); + return c.json({ ok: true }); + }); +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function getAuth(c: Context): ServiceAuth { + const a = c.get("serviceAuth") as ServiceAuth | undefined; + if (!a) throw new Error("service auth not set"); + return a; +} + +/** TID-ish ASCII lowercase base32-like identifier; good enough for space keys + * alongside the reserved `$`-prefixed ones. Mirrors the approach in spaces/tid.ts. */ +function generateKey(): string { + const chars = "234567abcdefghijklmnopqrstuvwxyz"; + const bytes = crypto.getRandomValues(new Uint8Array(13)); + let out = ""; + for (let i = 0; i < 13; i++) out += chars[bytes[i]! % 32]; + return out; +} + +async function bootstrapReservedSpaces(args: { + communityDid: string; + creatorDid: string; + spaces: SpacesAdapter; + community: CommunityAdapter; + type: string; + serviceDid: string; +}): Promise { + for (const key of RESERVED_KEYS) { + const uri = buildSpaceUri({ + ownerDid: args.communityDid, + type: args.type, + key, + }); + await args.spaces.createSpace({ + uri, + ownerDid: args.communityDid, + type: args.type, + key, + serviceDid: args.serviceDid, + appPolicyRef: null, + appPolicy: null, + }); + await args.community.grant({ + spaceUri: uri, + subjectDid: args.creatorDid, + accessLevel: "owner", + grantedBy: args.creatorDid, + }); + // Materialize membership: creator is in the space. + await args.spaces.applyMembershipDiff(uri, [args.creatorDid], [], args.creatorDid); + } +} diff --git a/src/core/community/schema.ts b/src/core/community/schema.ts new file mode 100644 index 0000000..ef5e16d --- /dev/null +++ b/src/core/community/schema.ts @@ -0,0 +1,40 @@ +import type { Database } from "../types"; +import { getDialect } from "../dialect"; +import type { SqlDialect } from "../dialect"; + +export function buildCommunitySchema(dialect: SqlDialect): string[] { + return [ + `CREATE TABLE IF NOT EXISTS communities ( + did TEXT PRIMARY KEY, + mode TEXT NOT NULL, + pds_endpoint TEXT, + app_password_encrypted TEXT, + identifier TEXT, + signing_key_encrypted TEXT, + rotation_key_encrypted TEXT, + created_by TEXT NOT NULL, + created_at ${dialect.bigintType} NOT NULL, + deleted_at ${dialect.bigintType} + )`, + `CREATE INDEX IF NOT EXISTS idx_communities_created_at ON communities(created_at DESC)`, + + `CREATE TABLE IF NOT EXISTS community_access_levels ( + space_uri TEXT NOT NULL, + subject TEXT NOT NULL, + subject_kind TEXT NOT NULL CHECK (subject_kind IN ('did', 'space')), + access_level TEXT NOT NULL, + granted_by TEXT NOT NULL, + granted_at ${dialect.bigintType} NOT NULL, + PRIMARY KEY (space_uri, subject) + )`, + `CREATE INDEX IF NOT EXISTS idx_cal_subject ON community_access_levels(subject)`, + `CREATE INDEX IF NOT EXISTS idx_cal_subject_space ON community_access_levels(subject) + WHERE subject_kind = 'space'`, + ]; +} + +export async function initCommunitySchema(db: Database): Promise { + const dialect = getDialect(db); + const stmts = buildCommunitySchema(dialect); + await db.batch(stmts.map((s) => db.prepare(s))); +} diff --git a/src/core/community/types.ts b/src/core/community/types.ts new file mode 100644 index 0000000..5b7bfe1 --- /dev/null +++ b/src/core/community/types.ts @@ -0,0 +1,75 @@ +import type { Database } from "../types"; +import type { DidDocumentResolver } from "@atcute/identity-resolver"; + +/** Access level a subject (did or group-space) has on a given space. + * Levels are totally ordered. Higher levels include lower levels' powers. + * See docs/community.md for the exact semantics. */ +export type AccessLevel = "member" | "manager" | "admin" | "owner"; + +export const ACCESS_LEVELS: readonly AccessLevel[] = [ + "member", + "manager", + "admin", + "owner", +] as const; + +export function rankOf(level: AccessLevel): number { + return ACCESS_LEVELS.indexOf(level); +} + +export function isAccessLevel(v: unknown): v is AccessLevel { + return typeof v === "string" && ACCESS_LEVELS.includes(v as AccessLevel); +} + +export type CommunityMode = "adopt" | "mint"; + +export interface CommunityConfig { + /** Service DID for JWT verification. Falls back to spaces.serviceDid when both modules are enabled. */ + serviceDid?: string; + /** PLC directory host for minted communities. */ + plcDirectory?: string; + /** Master key for envelope-encrypting stored credentials (app passwords, signing keys, rotation keys). + * Accepts a raw Uint8Array (preferred) or a base64/hex string that decodes to 32 bytes. */ + masterKey: Uint8Array | string; + /** Optional override for DID resolution (used during adopt to resolve identifier → DID → PDS). */ + resolver?: DidDocumentResolver; + /** Optional override for the fetch implementation (useful for tests). */ + fetch?: typeof fetch; +} + +export interface CommunityRow { + did: string; + mode: CommunityMode; + // adopt-mode fields + pdsEndpoint: string | null; + appPasswordEncrypted: Uint8Array | null; + identifier: string | null; + // mint-mode fields (Stage 4) + signingKeyEncrypted: Uint8Array | null; + rotationKeyEncrypted: Uint8Array | null; + // common + createdBy: string; + createdAt: number; + deletedAt: number | null; +} + +export interface AccessLevelRow { + spaceUri: string; + subjectDid: string | null; + subjectSpaceUri: string | null; + accessLevel: AccessLevel; + grantedBy: string; + grantedAt: number; +} + +/** Key prefix for reserved community-owned spaces. */ +export const RESERVED_KEYS = ["$admin", "$publishers"] as const; +export type ReservedKey = (typeof RESERVED_KEYS)[number]; + +export function isReservedKey(key: string): key is ReservedKey { + return (RESERVED_KEYS as readonly string[]).includes(key); +} + +export interface AdapterContext { + db: Database; +} diff --git a/src/core/db/schema.ts b/src/core/db/schema.ts index 38305f9..7bf06d7 100644 --- a/src/core/db/schema.ts +++ b/src/core/db/schema.ts @@ -11,6 +11,7 @@ import { } from "../types"; import { getSearchableFields } from "../search"; import { buildSpacesBaseSchema } from "../spaces/schema"; +import { buildCommunitySchema } from "../community/schema"; function getResolved(config: ContrailConfig): ResolvedMaps { return (config as ResolvedContrailConfig)._resolved ?? resolveConfig(config)._resolved; @@ -309,6 +310,13 @@ export async function initSchema( await applySpacesSchema(spacesSharesMainDb ? db : spacesDb!, config, dialect); } + if (config.community) { + // Community tables live on the same DB as spaces (they reference space_uri). + const target = spacesSharesMainDb ? db : spacesDb!; + const communityStmts = buildCommunitySchema(dialect); + await target.batch(communityStmts.map((s) => target.prepare(s))); + } + // FTS5 may not be available (e.g. node:sqlite) — skip gracefully for (const stmt of ftsStatements) { try { diff --git a/src/core/router/index.ts b/src/core/router/index.ts index cf54ad6..7ab98fa 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -8,10 +8,12 @@ import { registerFeedRoutes } from "./feed"; import { registerNotifyRoute } from "./notify"; import { registerSpacesRoutes } from "../spaces/router"; import type { SpacesRoutesOptions } from "../spaces/router"; -import { buildVerifier } from "../spaces/auth"; +import { buildVerifier, createServiceAuthMiddleware } from "../spaces/auth"; import { HostedAdapter } from "../spaces/adapter"; import type { StorageAdapter } from "../spaces/types"; import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; +import { registerCommunityRoutes } from "../community/router"; +import type { CommunityRoutesOptions } from "../community/router"; import { resolveActor } from "../identity"; import { resolveProfiles } from "./profiles"; import { backfillUser } from "../backfill"; @@ -23,6 +25,7 @@ export interface SpacesContext { export interface CreateAppOptions { spaces?: SpacesRoutesOptions; + community?: CommunityRoutesOptions; /** Separate DB for the spaces tables. Defaults to `db`. */ spacesDb?: Database; /** Full spaces context override (escape hatch for tests). */ @@ -85,5 +88,20 @@ export function createApp( registerNotifyRoute(app, db, config); registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx); + if (config.community && spacesCtx) { + // Community routes reuse the spaces service-auth middleware (same JWT verifier). + const authMiddleware = + options.community?.authMiddleware ?? + options.spaces?.authMiddleware ?? + createServiceAuthMiddleware(spacesCtx.verifier); + registerCommunityRoutes( + app, + spacesDb, + config, + { ...options.community, authMiddleware }, + { spacesAdapter: spacesCtx.adapter, verifier: spacesCtx.verifier } + ); + } + return app; } diff --git a/src/core/spaces/acl.ts b/src/core/spaces/acl.ts index 05adc40..0beafb0 100644 --- a/src/core/spaces/acl.ts +++ b/src/core/spaces/acl.ts @@ -1,4 +1,4 @@ -import type { AppPolicy, MemberPerm, SpaceMemberRow, SpaceRow } from "./types"; +import type { AppPolicy, SpaceMemberRow, SpaceRow } from "./types"; export type AclOp = "read" | "write" | "delete"; @@ -20,7 +20,6 @@ export type AclResult = export type AclDenyReason = | "not-member" - | "not-writer" | "not-own-record" | "app-not-allowed" | "unknown-op"; @@ -39,34 +38,26 @@ export function checkAppPolicy( const isOwner = (space: SpaceRow, did: string) => space.ownerDid === did; const hasMember = (space: SpaceRow, member: SpaceMemberRow | null, did: string) => isOwner(space, did) || member != null; -const hasWrite = (space: SpaceRow, member: SpaceMemberRow | null, did: string) => - isOwner(space, did) || member?.perms === "write"; /** Space-level access check. - * Model matches the proposal: member list is a (DID, perm) tuple set per space; - * write implies read; owner is always implicit write. No per-collection - * policies — all records in a space share the same access rule. */ + * Membership = access. Any member can read and write; the app filters + * writes it doesn't want on its own side. Delete keeps the owner/own-record + * rule so a random member can't nuke other people's records. */ export function checkAccess(input: AclInput): AclResult { if (!checkAppPolicy(input.space.appPolicy, input.clientId)) { return { allow: false, reason: "app-not-allowed" }; } - if (input.op === "read") { + if (input.op === "read" || input.op === "write") { return hasMember(input.space, input.member, input.callerDid) ? { allow: true } : { allow: false, reason: "not-member" }; } - if (input.op === "write") { - return hasWrite(input.space, input.member, input.callerDid) - ? { allow: true } - : { allow: false, reason: "not-writer" }; - } - if (input.op === "delete") { if (isOwner(input.space, input.callerDid)) return { allow: true }; - if (!hasWrite(input.space, input.member, input.callerDid)) { - return { allow: false, reason: "not-writer" }; + if (!hasMember(input.space, input.member, input.callerDid)) { + return { allow: false, reason: "not-member" }; } if (input.targetAuthorDid && input.targetAuthorDid !== input.callerDid) { return { allow: false, reason: "not-own-record" }; @@ -76,5 +67,3 @@ export function checkAccess(input: AclInput): AclResult { return { allow: false, reason: "unknown-op" }; } - -export type MemberPermExport = MemberPerm; diff --git a/src/core/spaces/adapter.ts b/src/core/spaces/adapter.ts index 67a0b8c..8c839d3 100644 --- a/src/core/spaces/adapter.ts +++ b/src/core/spaces/adapter.ts @@ -17,7 +17,6 @@ import type { ListOptions, ListResult, ListSpacesOptions, - MemberPerm, SpaceMemberRow, SpaceRow, StorageAdapter, @@ -48,7 +47,6 @@ function mapSpaceRow(row: any): SpaceRow { type: row.type, key: row.key, serviceDid: row.service_did, - memberListRef: row.member_list_ref ?? null, appPolicyRef: row.app_policy_ref ?? null, appPolicy: parseJson(row.app_policy), createdAt: toNum(row.created_at), @@ -60,7 +58,6 @@ function mapMemberRow(row: any): SpaceMemberRow { return { spaceUri: row.space_uri, did: row.did, - perms: row.perms as MemberPerm, addedAt: toNum(row.added_at), addedBy: row.added_by ?? null, }; @@ -71,7 +68,6 @@ function mapInviteRow(row: any): InviteRow { tokenHash: row.token_hash, spaceUri: row.space_uri, kind: (row.kind ?? "join") as InviteKind, - perms: row.perms, expiresAt: row.expires_at == null ? null : toNum(row.expires_at), maxUses: row.max_uses == null ? null : Number(row.max_uses), usedCount: Number(row.used_count), @@ -123,8 +119,8 @@ export class HostedAdapter implements StorageAdapter { const now = Date.now(); await this.db .prepare( - `INSERT INTO spaces (uri, owner_did, type, key, service_did, member_list_ref, app_policy_ref, app_policy, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO spaces (uri, owner_did, type, key, service_did, app_policy_ref, app_policy, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` ) .bind( space.uri, @@ -132,7 +128,6 @@ export class HostedAdapter implements StorageAdapter { space.type, space.key, space.serviceDid, - space.memberListRef, space.appPolicyRef, space.appPolicy ? JSON.stringify(space.appPolicy) : null, now @@ -203,14 +198,14 @@ export class HostedAdapter implements StorageAdapter { .run(); } - async addMember(spaceUri: string, did: string, perms: MemberPerm, addedBy: string | null): Promise { + async addMember(spaceUri: string, did: string, addedBy: string | null): Promise { await this.db .prepare( - `INSERT INTO spaces_members (space_uri, did, perms, added_at, added_by) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT (space_uri, did) DO UPDATE SET perms = excluded.perms` + `INSERT INTO spaces_members (space_uri, did, added_at, added_by) + VALUES (?, ?, ?, ?) + ON CONFLICT (space_uri, did) DO NOTHING` ) - .bind(spaceUri, did, perms, Date.now(), addedBy) + .bind(spaceUri, did, Date.now(), addedBy) .run(); } @@ -237,18 +232,48 @@ export class HostedAdapter implements StorageAdapter { return results.map(mapMemberRow); } + async applyMembershipDiff( + spaceUri: string, + adds: string[], + removes: string[], + addedBy: string | null + ): Promise { + const now = Date.now(); + const stmts: any[] = []; + for (const did of adds) { + stmts.push( + this.db + .prepare( + `INSERT INTO spaces_members (space_uri, did, added_at, added_by) + VALUES (?, ?, ?, ?) + ON CONFLICT (space_uri, did) DO NOTHING` + ) + .bind(spaceUri, did, now, addedBy) + ); + } + for (const did of removes) { + stmts.push( + this.db + .prepare(`DELETE FROM spaces_members WHERE space_uri = ? AND did = ?`) + .bind(spaceUri, did) + ); + } + if (stmts.length > 0) { + await this.db.batch(stmts); + } + } + async createInvite(input: CreateInviteInput): Promise { const now = Date.now(); await this.db .prepare( - `INSERT INTO spaces_invites (token_hash, space_uri, kind, perms, expires_at, max_uses, used_count, created_by, created_at, note) - VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)` + `INSERT INTO spaces_invites (token_hash, space_uri, kind, expires_at, max_uses, used_count, created_by, created_at, note) + VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?)` ) .bind( input.tokenHash, input.spaceUri, input.kind, - input.perms, input.expiresAt, input.maxUses, input.createdBy, @@ -260,7 +285,6 @@ export class HostedAdapter implements StorageAdapter { tokenHash: input.tokenHash, spaceUri: input.spaceUri, kind: input.kind, - perms: input.perms, expiresAt: input.expiresAt, maxUses: input.maxUses, usedCount: 0, diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index ef2c93f..0b2e188 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -13,7 +13,7 @@ import { import { nextTid } from "./tid"; import { generateInviteToken, hashInviteToken } from "./invite-token"; import { buildSpaceUri } from "./uri"; -import type { InviteKind, InviteRow, MemberPerm, SpaceRow, SpacesConfig, StorageAdapter } from "./types"; +import type { InviteKind, InviteRow, SpaceRow, SpacesConfig, StorageAdapter } from "./types"; import type { Did } from "@atcute/lexicons"; export interface SpacesRoutesOptions { @@ -273,7 +273,6 @@ export function registerSpacesRoutes( type?: string; key?: string; appPolicy?: SpaceRow["appPolicy"]; - memberListRef?: string; appPolicyRef?: string; }; @@ -290,12 +289,11 @@ export function registerSpacesRoutes( type, key, serviceDid: spacesConfig.serviceDid, - memberListRef: body.memberListRef ?? null, appPolicyRef: body.appPolicyRef ?? null, appPolicy: body.appPolicy ?? spacesConfig.defaultAppPolicy ?? null, }); // Owner is implicit; we still write a row so membership queries are uniform. - await adapter.addMember(uri, sa.issuer, "write", sa.issuer); + await adapter.addMember(uri, sa.issuer, sa.issuer); return c.json({ space: publicSpaceView(space, true) }); }); @@ -307,7 +305,6 @@ export function registerSpacesRoutes( | { spaceUri?: string; kind?: InviteKind; - perms?: MemberPerm; expiresAt?: number; maxUses?: number; note?: string; @@ -332,7 +329,6 @@ export function registerSpacesRoutes( spaceUri: body.spaceUri, tokenHash, kind, - perms: body.perms ?? "write", expiresAt: body.expiresAt ?? null, maxUses: body.maxUses ?? null, createdBy: sa.issuer, @@ -352,8 +348,8 @@ export function registerSpacesRoutes( if (!invite) { return c.json({ error: "InvalidInvite", reason: "expired-revoked-or-exhausted" }, 400); } - await adapter.addMember(invite.spaceUri, sa.issuer, invite.perms, invite.createdBy); - return c.json({ spaceUri: invite.spaceUri, perms: invite.perms }); + await adapter.addMember(invite.spaceUri, sa.issuer, invite.createdBy); + return c.json({ spaceUri: invite.spaceUri }); }); app.get(`/xrpc/${SPACE}.invite.list`, auth, async (c) => { @@ -390,7 +386,7 @@ export function registerSpacesRoutes( app.post(`/xrpc/${SPACE}.addMember`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => null)) as - | { spaceUri?: string; did?: string; perms?: MemberPerm } + | { spaceUri?: string; did?: string } | null; if (!body?.spaceUri || !body.did) { return c.json({ error: "InvalidRequest", message: "spaceUri and did required" }, 400); @@ -400,7 +396,7 @@ export function registerSpacesRoutes( if (space.ownerDid !== sa.issuer) { return c.json({ error: "Forbidden", reason: "not-owner" }, 403); } - await adapter.addMember(body.spaceUri, body.did, body.perms ?? "write", sa.issuer); + await adapter.addMember(body.spaceUri, body.did, sa.issuer); return c.json({ ok: true }); }); @@ -451,11 +447,11 @@ export function registerSpacesRoutes( const isOwner = space.ownerDid === sa.issuer; if (isOwner) { - return c.json({ isOwner: true, isMember: true, perms: "write" as const }); + return c.json({ isOwner: true, isMember: true }); } const member = await adapter.getMember(spaceUri, sa.issuer); if (!member) return c.json({ isOwner: false, isMember: false }); - return c.json({ isOwner: false, isMember: true, perms: member.perms }); + return c.json({ isOwner: false, isMember: true }); }); } @@ -475,7 +471,6 @@ function publicInviteView(invite: InviteRow) { tokenHash: invite.tokenHash, spaceUri: invite.spaceUri, kind: invite.kind, - perms: invite.perms, expiresAt: invite.expiresAt, maxUses: invite.maxUses, usedCount: invite.usedCount, @@ -493,7 +488,6 @@ function publicSpaceView(space: SpaceRow, forOwner: boolean) { type: space.type, key: space.key, serviceDid: space.serviceDid, - memberListRef: space.memberListRef, appPolicyRef: space.appPolicyRef, createdAt: space.createdAt, ...(forOwner ? { appPolicy: space.appPolicy } : {}), diff --git a/src/core/spaces/schema.ts b/src/core/spaces/schema.ts index f46c909..d6cf4a1 100644 --- a/src/core/spaces/schema.ts +++ b/src/core/spaces/schema.ts @@ -17,7 +17,6 @@ export function buildSpacesBaseSchema(dialect: SqlDialect): string[] { type TEXT NOT NULL, key TEXT NOT NULL, service_did TEXT NOT NULL, - member_list_ref TEXT, app_policy_ref TEXT, app_policy ${dialect.recordColumnType}, created_at ${dialect.bigintType} NOT NULL, @@ -29,7 +28,6 @@ export function buildSpacesBaseSchema(dialect: SqlDialect): string[] { `CREATE TABLE IF NOT EXISTS spaces_members ( space_uri TEXT NOT NULL, did TEXT NOT NULL, - perms TEXT NOT NULL, added_at ${dialect.bigintType} NOT NULL, added_by TEXT, PRIMARY KEY (space_uri, did) @@ -40,7 +38,6 @@ export function buildSpacesBaseSchema(dialect: SqlDialect): string[] { token_hash TEXT PRIMARY KEY, space_uri TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'join', - perms TEXT NOT NULL, expires_at ${dialect.bigintType}, max_uses INTEGER, used_count INTEGER NOT NULL DEFAULT 0, diff --git a/src/core/spaces/types.ts b/src/core/spaces/types.ts index cb166e2..ff5584f 100644 --- a/src/core/spaces/types.ts +++ b/src/core/spaces/types.ts @@ -1,9 +1,6 @@ import type { Database } from "../types"; import type { DidDocumentResolver } from "@atcute/identity-resolver"; -/** Member permission in a space. "write" implies "read"; owner is always implicit write. */ -export type MemberPerm = "read" | "write"; - export type AppPolicyMode = "allow" | "deny"; export interface AppPolicy { @@ -29,7 +26,6 @@ export interface SpaceRow { type: string; key: string; serviceDid: string; - memberListRef: string | null; appPolicyRef: string | null; appPolicy: AppPolicy | null; createdAt: number; @@ -39,7 +35,6 @@ export interface SpaceRow { export interface SpaceMemberRow { spaceUri: string; did: string; - perms: MemberPerm; addedAt: number; addedBy: string | null; } @@ -79,7 +74,7 @@ export interface CollectionCount { } /** What a token holder can do with this invite. - * - `'join'`: must be redeemed while signed in; becomes a member with `perms`. + * - `'join'`: must be redeemed while signed in; redeemer becomes a member. * - `'read'`: bearer-only — token itself grants read access to the space; cannot be redeemed. * - `'read-join'`: both — anonymous holders read; signed-in holders may also redeem to join. */ export type InviteKind = "join" | "read" | "read-join"; @@ -88,7 +83,6 @@ export interface InviteRow { tokenHash: string; spaceUri: string; kind: InviteKind; - perms: MemberPerm; expiresAt: number | null; maxUses: number | null; usedCount: number; @@ -102,7 +96,6 @@ export interface CreateInviteInput { spaceUri: string; tokenHash: string; kind: InviteKind; - perms: MemberPerm; expiresAt: number | null; maxUses: number | null; createdBy: string; @@ -111,7 +104,6 @@ export interface CreateInviteInput { export interface RedeemInviteResult { spaceUri: string; - perms: MemberPerm; } export interface StorageAdapter { @@ -123,10 +115,18 @@ export interface StorageAdapter { updateSpaceAppPolicy(spaceUri: string, appPolicy: AppPolicy): Promise; // Members - addMember(spaceUri: string, did: string, perms: MemberPerm, addedBy: string | null): Promise; + addMember(spaceUri: string, did: string, addedBy: string | null): Promise; removeMember(spaceUri: string, did: string): Promise; getMember(spaceUri: string, did: string): Promise; listMembers(spaceUri: string): Promise; + /** Bulk-apply a membership diff. Used only by the community module's reconciler; + * not exposed as an XRPC endpoint. */ + applyMembershipDiff( + spaceUri: string, + adds: string[], + removes: string[], + addedBy: string | null + ): Promise; // Invites createInvite(input: CreateInviteInput): Promise; diff --git a/src/core/types.ts b/src/core/types.ts index 56ee37d..bbdeef2 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -155,6 +155,9 @@ export interface ContrailConfig { notify?: boolean | string; /** Permissioned spaces configuration. When set, the service exposes space XRPCs. */ spaces?: import("./spaces/types").SpacesConfig; + /** Community module configuration. When set, the service exposes community XRPCs + * for managing community-owned spaces and tiered access levels. Requires `spaces`. */ + community?: import("./community/types").CommunityConfig; /** Customize the auto-generated `.permissionSet` lexicon. */ permissionSet?: PermissionSetConfig; } diff --git a/src/generate.ts b/src/generate.ts index 5a77c8a..6ecf411 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -40,6 +40,17 @@ function findSpaceTemplatesDir(rootDir: string): string | null { return null; } +function findCommunityTemplatesDir(rootDir: string): string | null { + const candidates = [ + join(rootDir, "community-lexicon-templates"), + join(rootDir, "node_modules/@atmo-dev/contrail/community-lexicon-templates"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} + /** Yield all JSON files under a directory (recursive). */ function* walkJson(dir: string): Generator { for (const entry of readdirSync(dir, { withFileTypes: true })) { @@ -721,6 +732,48 @@ export function generateLexicons(options: GenerateOptions): Record.community.* --- + + if (config.community) { + log("Generating community endpoints..."); + const templatesDir = findCommunityTemplatesDir(rootDir); + if (!templatesDir) { + log(" (community templates not found — skipping)"); + } else { + const templateIdRe = /^tools\.atmo\.community(\.[A-Za-z0-9.]+)?$/; + const idReplace = (id: string) => + id.startsWith("tools.atmo.community") + ? id.replace(/^tools\.atmo\.community/, `${ns}.community`) + : id; + + const rewriteRefs = (obj: any): any => { + if (Array.isArray(obj)) return obj.map(rewriteRefs); + if (obj && typeof obj === "object") { + const out: any = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === "ref" && typeof v === "string" && v.startsWith("tools.atmo.community")) { + out[k] = v.replace(/^tools\.atmo\.community/, `${ns}.community`); + } else if (k === "id" && typeof v === "string" && templateIdRe.test(v)) { + out[k] = idReplace(v); + } else { + out[k] = rewriteRefs(v); + } + } + return out; + } + return obj; + }; + + for (const file of walkJson(templatesDir)) { + const doc = JSON.parse(readFileSync(file, "utf-8")); + if (typeof doc.id !== "string" || !templateIdRe.test(doc.id)) continue; + const newId = idReplace(doc.id); + const rewritten = rewriteRefs({ ...doc, id: newId }); + writeLexicon(newId, rewritten); + } + } + } + // --- Permission set --- // Permission-set lexicons (https://atproto.com/guides/permission-sets) can // only reference NSIDs under the same namespace as the set itself, which diff --git a/src/index.ts b/src/index.ts index 7eebb87..1311579 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ export type { PersistentIngestOptions } from "./core/persistent"; // Spaces export type { SpacesConfig, - MemberPerm, AppPolicy, AppPolicyMode, SpaceRow, diff --git a/tests/community-delegation.test.ts b/tests/community-delegation.test.ts new file mode 100644 index 0000000..e6d366b --- /dev/null +++ b/tests/community-delegation.test.ts @@ -0,0 +1,246 @@ +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; +const CHARLIE = "did:plc:charlie"; +const DIANA = "did:plc:diana"; +const COMMUNITY_DID = "did:plc:acme"; +const PDS_ENDPOINT = "https://pds.example"; + +const MASTER_KEY = new Uint8Array(32).fill(11); + +const CONFIG: ContrailConfig = { + namespace: "test.comm", + collections: { message: { collection: "app.event.message" } }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:test.example#svc", + }, + community: { + masterKey: MASTER_KEY, + fetch: mockFetch, + resolver: mockResolver(), + }, +}; + +function mockResolver(): any { + return { + resolve: async (did: string) => { + if (did !== COMMUNITY_DID) throw new Error("unknown did"); + return { + id: did, + service: [ + { + id: "#atproto_pds", + type: "AtprotoPersonalDataServer", + serviceEndpoint: PDS_ENDPOINT, + }, + ], + }; + }, + }; +} + +async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith("/xrpc/com.atproto.server.createSession") && init?.method === "POST") { + return new Response( + JSON.stringify({ accessJwt: "a.b.c", refreshJwt: "r.r.r", did: COMMUNITY_DID }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + return new Response("not found", { status: 404 }); +} + +function fakeAuth(): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { issuer: did, audience: CONFIG.spaces!.serviceDid, lxm: undefined }); + await next(); + }; +} + +async function makeApp(): Promise { + const db = createSqliteDatabase(":memory:"); + const resolved = resolveConfig(CONFIG); + await initSchema(db, resolved); + return createApp(db, resolved, { spaces: { authMiddleware: fakeAuth() } }); +} + +function call( + app: Hono, + method: string, + path: string, + did: string, + body?: any +): Promise { + const headers: Record = { "X-Test-Did": did }; + if (body !== undefined) headers["Content-Type"] = "application/json"; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + ); +} + +async function adopt(app: Hono, caller: string) { + const res = await call(app, "POST", "/xrpc/test.comm.community.adopt", caller, { + identifier: COMMUNITY_DID, + appPassword: "ok", + }); + expect(res.status).toBe(200); +} + +async function createSpace(app: Hono, caller: string, key: string): Promise { + const res = await call(app, "POST", "/xrpc/test.comm.community.space.create", caller, { + communityDid: COMMUNITY_DID, + key, + }); + expect(res.status).toBe(200); + return ((await res.json()) as any).space.uri; +} + +async function grant(app: Hono, caller: string, spaceUri: string, subject: any, accessLevel: string) { + const res = await call(app, "POST", "/xrpc/test.comm.community.space.grant", caller, { + spaceUri, + subject, + accessLevel, + }); + return res; +} + +async function whoamiLevel(app: Hono, caller: string, spaceUri: string): Promise { + const res = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, caller); + expect(res.status).toBe(200); + return ((await res.json()) as any).accessLevel; +} + +async function flatMembers(app: Hono, caller: string, spaceUri: string): Promise { + const res = await call( + app, + "GET", + `/xrpc/test.comm.community.space.listMembers?spaceUri=${encodeURIComponent(spaceUri)}&flatten=true`, + caller + ); + expect(res.status).toBe(200); + return ((await res.json()) as any).members.map((m: any) => m.did); +} + +describe("community delegation — stage 2", () => { + let app: Hono; + + beforeAll(async () => { + app = await makeApp(); + await adopt(app, ALICE); + }); + + it("subject_space_uri delegates membership from one space to another", async () => { + const mods = await createSpace(app, ALICE, "mods"); + const chat = await createSpace(app, ALICE, "mod-chat"); + + // Bob is a member of mods. + expect((await grant(app, ALICE, mods, { did: BOB }, "member")).status).toBe(200); + // mods is a member of mod-chat. + expect((await grant(app, ALICE, chat, { spaceUri: mods }, "member")).status).toBe(200); + + // Bob is transitively a member of mod-chat. + expect(await whoamiLevel(app, BOB, chat)).toBe("member"); + const flat = await flatMembers(app, ALICE, chat); + expect(flat).toContain(BOB); + expect(flat).toContain(ALICE); // owner + }); + + it("access is capped at the path minimum (delegation can only reduce)", async () => { + const mods = await createSpace(app, ALICE, "mods2"); + const target = await createSpace(app, ALICE, "target2"); + + // Bob is an owner of mods2. + expect((await grant(app, ALICE, mods, { did: BOB }, "owner")).status).toBe(200); + // mods2 is a `member` of target2 (capped path). + expect((await grant(app, ALICE, target, { spaceUri: mods }, "member")).status).toBe(200); + + // Bob's effective level in target2 is `member`, not `owner`. + expect(await whoamiLevel(app, BOB, target)).toBe("member"); + }); + + it("cycle detection rejects A → B → A", async () => { + const a = await createSpace(app, ALICE, "cycle-a"); + const b = await createSpace(app, ALICE, "cycle-b"); + + expect((await grant(app, ALICE, a, { spaceUri: b }, "member")).status).toBe(200); + // Now creating b → a would close the cycle. + const res = await grant(app, ALICE, b, { spaceUri: a }, "member"); + expect(res.status).toBe(400); + expect(((await res.json()) as any).reason).toBe("cycle-detected"); + }); + + it("rejects self-reference", async () => { + const s = await createSpace(app, ALICE, "self-ref"); + const res = await grant(app, ALICE, s, { spaceUri: s }, "member"); + expect(res.status).toBe(400); + expect(((await res.json()) as any).reason).toBe("self-reference"); + }); + + it("reverse-graph reconcile: adding to source propagates to delegated spaces", async () => { + const mods = await createSpace(app, ALICE, "mods3"); + const chat1 = await createSpace(app, ALICE, "chat-a"); + const chat2 = await createSpace(app, ALICE, "chat-b"); + + // Both chats delegate to mods3. + expect((await grant(app, ALICE, chat1, { spaceUri: mods }, "member")).status).toBe(200); + expect((await grant(app, ALICE, chat2, { spaceUri: mods }, "member")).status).toBe(200); + + // Add Charlie to mods3 — should propagate to both chats. + expect((await grant(app, ALICE, mods, { did: CHARLIE }, "member")).status).toBe(200); + + expect(await flatMembers(app, ALICE, chat1)).toContain(CHARLIE); + expect(await flatMembers(app, ALICE, chat2)).toContain(CHARLIE); + }); + + it("reverse-graph reconcile: removing from source propagates to delegated spaces", async () => { + const mods = await createSpace(app, ALICE, "mods4"); + const chat = await createSpace(app, ALICE, "chat-c"); + + expect((await grant(app, ALICE, chat, { spaceUri: mods }, "member")).status).toBe(200); + expect((await grant(app, ALICE, mods, { did: DIANA }, "member")).status).toBe(200); + expect(await flatMembers(app, ALICE, chat)).toContain(DIANA); + + // Revoke from mods4 — should disappear from chat-c. + const res = await call(app, "POST", "/xrpc/test.comm.community.space.revoke", ALICE, { + spaceUri: mods, + subject: { did: DIANA }, + }); + expect(res.status).toBe(200); + + expect(await flatMembers(app, ALICE, chat)).not.toContain(DIANA); + }); + + it("resync endpoint requires admin+", async () => { + const s = await createSpace(app, ALICE, "resync-space"); + // Bob is a plain member + expect((await grant(app, ALICE, s, { did: BOB }, "member")).status).toBe(200); + + const res = await call(app, "POST", "/xrpc/test.comm.community.space.resync", BOB, { + spaceUri: s, + }); + expect(res.status).toBe(403); + }); + + it("resync endpoint works for owner", async () => { + const s = await createSpace(app, ALICE, "resync-ok"); + const res = await call(app, "POST", "/xrpc/test.comm.community.space.resync", ALICE, { + spaceUri: s, + }); + expect(res.status).toBe(200); + }); +}); diff --git a/tests/community-e2e.test.ts b/tests/community-e2e.test.ts new file mode 100644 index 0000000..3acb71e --- /dev/null +++ b/tests/community-e2e.test.ts @@ -0,0 +1,266 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; +const CHARLIE = "did:plc:charlie"; +const COMMUNITY_DID = "did:plc:acmecommunity"; +const PDS_ENDPOINT = "https://pds.example"; + +// 32-byte test master key (deterministic; not a real secret). +const MASTER_KEY = new Uint8Array(32).fill(7); + +const CONFIG: ContrailConfig = { + namespace: "test.comm", + collections: { + message: { collection: "app.event.message" }, + }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:test.example#svc", + }, + community: { + masterKey: MASTER_KEY, + // Fake network: hand-rolled below. + fetch: mockFetch, + resolver: mockResolver(), + }, +}; + +function mockResolver(): any { + return { + resolve: async (did: string) => { + if (did !== COMMUNITY_DID) throw new Error("unknown did"); + return { + id: did, + service: [ + { + id: "#atproto_pds", + type: "AtprotoPersonalDataServer", + serviceEndpoint: PDS_ENDPOINT, + }, + ], + }; + }, + }; +} + +async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + // Handle resolution paths are handled by the mock resolver, not fetch. + if (url.endsWith("/xrpc/com.atproto.server.createSession") && init?.method === "POST") { + const body = JSON.parse((init.body as string) ?? "{}"); + if (body.password === "app-password-ok") { + return new Response( + JSON.stringify({ + accessJwt: "a.b.c", + refreshJwt: "r.r.r", + did: COMMUNITY_DID, + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + return new Response(JSON.stringify({ error: "AuthFactorTokenRequired" }), { status: 401 }); + } + return new Response("not found", { status: 404 }); +} + +function fakeAuth(): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { + issuer: did, + audience: CONFIG.spaces!.serviceDid, + lxm: undefined, + }); + await next(); + }; +} + +async function makeApp(): Promise { + const db = createSqliteDatabase(":memory:"); + const resolved = resolveConfig(CONFIG); + await initSchema(db, resolved); + return createApp(db, resolved, { spaces: { authMiddleware: fakeAuth() } }); +} + +function call( + app: Hono, + method: string, + path: string, + did: string | null, + body?: any +): Promise { + const headers: Record = {}; + if (did) headers["X-Test-Did"] = did; + if (body !== undefined) headers["Content-Type"] = "application/json"; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + ); +} + +async function adopt(app: Hono, caller: string) { + const res = await call(app, "POST", "/xrpc/test.comm.community.adopt", caller, { + identifier: COMMUNITY_DID, + appPassword: "app-password-ok", + }); + expect(res.status).toBe(200); + const body = (await res.json()) as { communityDid: string }; + expect(body.communityDid).toBe(COMMUNITY_DID); + return body.communityDid; +} + +describe("community e2e — stage 1", () => { + let app: Hono; + + beforeAll(async () => { + app = await makeApp(); + }); + + it("adopts a community and creates reserved spaces with creator as owner", async () => { + const did = await adopt(app, ALICE); + + const adminUri = `at://${did}/tools.atmo.event.space/$admin`; + const publishersUri = `at://${did}/tools.atmo.event.space/$publishers`; + + // whoami in both reserved spaces → owner + for (const uri of [adminUri, publishersUri]) { + const r = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(uri)}`, ALICE); + expect(r.status).toBe(200); + expect(((await r.json()) as any).accessLevel).toBe("owner"); + } + + // Bob isn't in either + const r = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(adminUri)}`, BOB); + expect(r.status).toBe(200); + expect(((await r.json()) as any).accessLevel).toBe(null); + }); + + it("rejects bad credentials", async () => { + const app2 = await makeApp(); + const res = await call(app2, "POST", "/xrpc/test.comm.community.adopt", ALICE, { + identifier: COMMUNITY_DID, + appPassword: "wrong", + }); + expect(res.status).toBe(401); + }); + + it("rejects duplicate adoption", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.adopt", BOB, { + identifier: COMMUNITY_DID, + appPassword: "app-password-ok", + }); + expect(res.status).toBe(409); + }); + + it("creates a non-reserved space via community.space.create", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "general", + }); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body.space.uri).toBe(`at://${COMMUNITY_DID}/tools.atmo.event.space/general`); + expect(body.space.ownerDid).toBe(COMMUNITY_DID); + }); + + it("rejects reserved keys in space.create", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "$admin", + }); + expect(res.status).toBe(400); + expect(((await res.json()) as any).reason).toBe("reserved-key"); + }); + + it("non-admin cannot create a space", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.space.create", BOB, { + communityDid: COMMUNITY_DID, + key: "random", + }); + expect(res.status).toBe(403); + }); + + it("owner grants Bob member access to #general; reconciler populates spaces_members", async () => { + const spaceUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/general`; + const res = await call(app, "POST", "/xrpc/test.comm.community.space.grant", ALICE, { + spaceUri, + subject: { did: BOB }, + accessLevel: "member", + }); + expect(res.status).toBe(200); + + // Bob can now see the members list + const list = await call( + app, + "GET", + `/xrpc/test.comm.community.space.listMembers?spaceUri=${encodeURIComponent(spaceUri)}`, + BOB + ); + expect(list.status).toBe(200); + const body = (await list.json()) as any; + expect(body.rows.map((r: any) => r.subject.did)).toContain(BOB); + }); + + it("manager cannot grant higher than own level", async () => { + const spaceUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/general`; + // Promote Bob to manager + await call(app, "POST", "/xrpc/test.comm.community.space.grant", ALICE, { + spaceUri, + subject: { did: BOB }, + accessLevel: "manager", + }); + // Bob tries to grant Charlie owner — should fail + const res = await call(app, "POST", "/xrpc/test.comm.community.space.grant", BOB, { + spaceUri, + subject: { did: CHARLIE }, + accessLevel: "owner", + }); + expect(res.status).toBe(403); + expect(((await res.json()) as any).reason).toBe("cannot-grant-higher-than-self"); + }); + + it("revokes and reconciler removes from spaces_members", async () => { + const spaceUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/general`; + const res = await call(app, "POST", "/xrpc/test.comm.community.space.revoke", ALICE, { + spaceUri, + subject: { did: BOB }, + }); + expect(res.status).toBe(200); + + const level = await call( + app, + "GET", + `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, + BOB + ); + expect(((await level.json()) as any).accessLevel).toBe(null); + }); + + it("cannot delete a reserved space", async () => { + const adminUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/$admin`; + const res = await call(app, "POST", "/xrpc/test.comm.community.space.delete", ALICE, { + spaceUri: adminUri, + }); + expect(res.status).toBe(403); + expect(((await res.json()) as any).reason).toBe("reserved-space-cannot-be-deleted"); + }); + + it("lists communities for an actor", async () => { + const res = await call(app, "GET", `/xrpc/test.comm.community.list`, ALICE); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body.communities.map((c: any) => c.did)).toContain(COMMUNITY_DID); + }); +}); diff --git a/tests/community-mint.test.ts b/tests/community-mint.test.ts new file mode 100644 index 0000000..96922a6 --- /dev/null +++ b/tests/community-mint.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; +import { + buildGenesisOp, + computeDidPlc, + encodeDagCbor, + generateKeyPair, + jwkToDidKey, + signGenesisOp, +} from "../src/core/community/plc"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; + +const MASTER_KEY = new Uint8Array(32).fill(99); + +/** Captures requests that would have gone to plc.directory. */ +const plcCalls: Array<{ url: string; method: string; body: any }> = []; + +const CONFIG: ContrailConfig = { + namespace: "test.comm", + collections: { message: { collection: "app.event.message" } }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:test.example#svc", + }, + community: { + masterKey: MASTER_KEY, + plcDirectory: "https://plc.test", + fetch: mockFetch, + }, +}; + +async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const method = init?.method ?? "GET"; + const body = init?.body ? JSON.parse(init.body as string) : {}; + if (url.startsWith("https://plc.test/")) { + plcCalls.push({ url, method, body }); + return new Response("{}", { status: 200 }); + } + return new Response("not found", { status: 404 }); +} + +function fakeAuth(): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { issuer: did, audience: CONFIG.spaces!.serviceDid, lxm: undefined }); + await next(); + }; +} + +async function makeApp(): Promise { + const db = createSqliteDatabase(":memory:"); + const resolved = resolveConfig(CONFIG); + await initSchema(db, resolved); + return createApp(db, resolved, { spaces: { authMiddleware: fakeAuth() } }); +} + +function call( + app: Hono, + method: string, + path: string, + did: string, + body?: any +): Promise { + const headers: Record = { "X-Test-Did": did }; + if (body !== undefined) headers["Content-Type"] = "application/json"; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + ); +} + +describe("plc op encoding (unit)", () => { + it("encodes canonical CBOR with sorted map keys", () => { + // Maps must be sorted by key length first, then lexicographically. + const out = encodeDagCbor({ b: 1, a: 2, ab: 3 }); + // Expected order: a, b, ab (short keys first). + // Header byte: major 5 (map), count 3 → 0xa3. + expect(out[0]).toBe(0xa3); + // First key should be "a" (0x61 text-string head len 1, then 0x61). + expect(out[1]).toBe(0x61); + expect(out[2]).toBe(0x61); + }); + + it("generates a P-256 keypair and a did:key", async () => { + const pair = await generateKeyPair(); + expect(pair.publicDidKey).toMatch(/^did:key:z/); + expect(pair.privateJwk.kty).toBe("EC"); + expect(pair.privateJwk.crv).toBe("P-256"); + }); + + it("computeDidPlc returns a stable did:plc", async () => { + const signing = await generateKeyPair(); + const rotation = await generateKeyPair(); + const unsigned = buildGenesisOp({ + rotationKeys: [rotation.publicDidKey], + verificationMethodAtproto: signing.publicDidKey, + }); + const signed = await signGenesisOp(unsigned, rotation.privateJwk); + const did = await computeDidPlc(signed); + expect(did).toMatch(/^did:plc:[a-z2-7]{24}$/); + }); + + it("jwkToDidKey: public key roundtrip shape", async () => { + const pair = await generateKeyPair(); + const k = jwkToDidKey(pair.privateJwk); // private JWK carries the pub coords + expect(k).toBe(pair.publicDidKey); + }); +}); + +describe("community.mint — stage 4", () => { + let app: Hono; + + beforeAll(async () => { + app = await makeApp(); + }); + + it("mints a community, returns recovery key, submits to PLC, bootstraps reserved spaces", async () => { + const before = plcCalls.length; + const res = await call(app, "POST", "/xrpc/test.comm.community.mint", ALICE, {}); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + + expect(body.communityDid).toMatch(/^did:plc:[a-z2-7]{24}$/); + // Recovery key is a private JWK returned once. + expect(body.recoveryKey.kty).toBe("EC"); + expect(body.recoveryKey.crv).toBe("P-256"); + expect(body.recoveryKey.d).toBeTruthy(); // private scalar + + // PLC submission happened. + const ourCalls = plcCalls.slice(before); + expect(ourCalls).toHaveLength(1); + expect(ourCalls[0]!.method).toBe("POST"); + expect(ourCalls[0]!.url).toBe(`https://plc.test/${body.communityDid}`); + expect(ourCalls[0]!.body.type).toBe("plc_operation"); + expect(ourCalls[0]!.body.sig).toBeTruthy(); + expect(ourCalls[0]!.body.rotationKeys).toHaveLength(2); + + // Reserved spaces exist with the caller as owner. + const adminUri = `at://${body.communityDid}/tools.atmo.event.space/$admin`; + const whoami = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(adminUri)}`, ALICE); + expect(((await whoami.json()) as any).accessLevel).toBe("owner"); + }); + + it("minted community rejects publishing (no PDS)", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.mint", ALICE, {}); + const { communityDid } = (await res.json()) as any; + + const pub = await call(app, "POST", "/xrpc/test.comm.community.putRecord", ALICE, { + communityDid, + collection: "app.event.message", + record: { text: "nope" }, + }); + expect(pub.status).toBe(400); + expect(((await pub.json()) as any).reason).toBe("publishing-not-supported-for-minted-communities"); + }); + + it("multiple mints produce distinct DIDs", async () => { + const r1 = await call(app, "POST", "/xrpc/test.comm.community.mint", BOB, {}); + const r2 = await call(app, "POST", "/xrpc/test.comm.community.mint", BOB, {}); + const d1 = ((await r1.json()) as any).communityDid; + const d2 = ((await r2.json()) as any).communityDid; + expect(d1).not.toBe(d2); + }); +}); diff --git a/tests/community-publishing.test.ts b/tests/community-publishing.test.ts new file mode 100644 index 0000000..5c97c2b --- /dev/null +++ b/tests/community-publishing.test.ts @@ -0,0 +1,267 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; +const CHARLIE = "did:plc:charlie"; +const COMMUNITY_DID = "did:plc:pubcomm"; +const PDS_ENDPOINT = "https://pds.example"; + +const MASTER_KEY = new Uint8Array(32).fill(42); + +/** Shared state for asserting PDS proxying happened. */ +const pdsCalls: Array<{ url: string; body: any }> = []; + +const CONFIG: ContrailConfig = { + namespace: "test.comm", + collections: { message: { collection: "app.event.message" } }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:test.example#svc", + }, + community: { + masterKey: MASTER_KEY, + fetch: mockFetch, + resolver: mockResolver(), + }, +}; + +function mockResolver(): any { + return { + resolve: async (did: string) => { + if (did !== COMMUNITY_DID) throw new Error("unknown did"); + return { + id: did, + service: [ + { + id: "#atproto_pds", + type: "AtprotoPersonalDataServer", + serviceEndpoint: PDS_ENDPOINT, + }, + ], + }; + }, + }; +} + +async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const body = init?.body ? JSON.parse(init.body as string) : {}; + pdsCalls.push({ url, body }); + if (url.endsWith("/xrpc/com.atproto.server.createSession")) { + if (body.password === "correct-pw" || body.password === "new-correct-pw") { + return new Response( + JSON.stringify({ accessJwt: "a.b.c", refreshJwt: "r.r.r", did: COMMUNITY_DID }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + return new Response(JSON.stringify({ error: "AuthFailed" }), { status: 401 }); + } + if (url.endsWith("/xrpc/com.atproto.repo.createRecord")) { + return new Response( + JSON.stringify({ + uri: `at://${COMMUNITY_DID}/${body.collection}/fakerkey`, + cid: "bafyfake", + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + if (url.endsWith("/xrpc/com.atproto.repo.deleteRecord")) { + return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response("not found", { status: 404 }); +} + +function fakeAuth(): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { issuer: did, audience: CONFIG.spaces!.serviceDid, lxm: undefined }); + await next(); + }; +} + +async function makeApp(): Promise { + const db = createSqliteDatabase(":memory:"); + const resolved = resolveConfig(CONFIG); + await initSchema(db, resolved); + return createApp(db, resolved, { spaces: { authMiddleware: fakeAuth() } }); +} + +function call( + app: Hono, + method: string, + path: string, + did: string, + body?: any +): Promise { + const headers: Record = { "X-Test-Did": did }; + if (body !== undefined) headers["Content-Type"] = "application/json"; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + ); +} + +async function adopt(app: Hono, caller: string, password: string) { + const res = await call(app, "POST", "/xrpc/test.comm.community.adopt", caller, { + identifier: COMMUNITY_DID, + appPassword: password, + }); + expect(res.status).toBe(200); +} + +async function grant(app: Hono, caller: string, spaceUri: string, subject: any, accessLevel: string) { + const res = await call(app, "POST", "/xrpc/test.comm.community.space.grant", caller, { + spaceUri, + subject, + accessLevel, + }); + expect(res.status).toBe(200); + return res; +} + +describe("community publishing + reauth — stage 3", () => { + let app: Hono; + const publishers = `at://${COMMUNITY_DID}/tools.atmo.event.space/$publishers`; + const admin = `at://${COMMUNITY_DID}/tools.atmo.event.space/$admin`; + + beforeAll(async () => { + app = await makeApp(); + await adopt(app, ALICE, "correct-pw"); + }); + + it("community.putRecord proxies to PDS for a $publishers member", async () => { + // Alice is already owner of $publishers from bootstrap; add Bob as plain member. + await grant(app, ALICE, publishers, { did: BOB }, "member"); + + const before = pdsCalls.length; + const res = await call(app, "POST", "/xrpc/test.comm.community.putRecord", BOB, { + communityDid: COMMUNITY_DID, + collection: "app.event.message", + record: { text: "hello from the community" }, + }); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body.uri).toMatch(/^at:\/\//); + + const newCalls = pdsCalls.slice(before); + expect(newCalls.some((c) => c.url.endsWith("/xrpc/com.atproto.server.createSession"))).toBe(true); + expect(newCalls.some((c) => c.url.endsWith("/xrpc/com.atproto.repo.createRecord"))).toBe(true); + }); + + it("non-$publishers member cannot putRecord", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.putRecord", CHARLIE, { + communityDid: COMMUNITY_DID, + collection: "app.event.message", + record: { text: "nope" }, + }); + expect(res.status).toBe(403); + expect(((await res.json()) as any).reason).toBe("not-in-publishers"); + }); + + it("community.space.putRecord writes in-space with community author (admin+ required)", async () => { + // Create a content space. + const createRes = await call(app, "POST", "/xrpc/test.comm.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "announcements", + }); + expect(createRes.status).toBe(200); + const spaceUri = ((await createRes.json()) as any).space.uri; + + // Alice (owner, which ≥ admin) writes. + const put = await call(app, "POST", "/xrpc/test.comm.community.space.putRecord", ALICE, { + spaceUri, + collection: "app.event.message", + record: { text: "first post" }, + }); + expect(put.status).toBe(200); + const body = (await put.json()) as any; + expect(body.authorDid).toBe(COMMUNITY_DID); + }); + + it("community.space.putRecord rejects non-admin members", async () => { + const createRes = await call(app, "POST", "/xrpc/test.comm.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "ann2", + }); + const spaceUri = ((await createRes.json()) as any).space.uri; + await grant(app, ALICE, spaceUri, { did: BOB }, "member"); + + const put = await call(app, "POST", "/xrpc/test.comm.community.space.putRecord", BOB, { + spaceUri, + collection: "app.event.message", + record: { text: "nope" }, + }); + expect(put.status).toBe(403); + }); + + it("setAccessLevel with rank-outranking", async () => { + const createRes = await call(app, "POST", "/xrpc/test.comm.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "roles-test", + }); + const spaceUri = ((await createRes.json()) as any).space.uri; + await grant(app, ALICE, spaceUri, { did: BOB }, "manager"); + await grant(app, ALICE, spaceUri, { did: CHARLIE }, "member"); + + // Bob (manager) promotes Charlie to manager — OK. + const ok = await call(app, "POST", "/xrpc/test.comm.community.space.setAccessLevel", BOB, { + spaceUri, + subject: { did: CHARLIE }, + accessLevel: "manager", + }); + expect(ok.status).toBe(200); + + // Bob (manager) tries to promote Charlie to admin — rejected. + const nope = await call(app, "POST", "/xrpc/test.comm.community.space.setAccessLevel", BOB, { + spaceUri, + subject: { did: CHARLIE }, + accessLevel: "admin", + }); + expect(nope.status).toBe(403); + }); + + it("getHealth reports healthy", async () => { + const res = await call(app, "GET", `/xrpc/test.comm.community.getHealth?communityDid=${COMMUNITY_DID}`, ALICE); + expect(res.status).toBe(200); + expect(((await res.json()) as any).status).toBe("healthy"); + }); + + it("reauth replaces the stored app password; old password no longer works", async () => { + const reauth = await call(app, "POST", "/xrpc/test.comm.community.reauth", ALICE, { + communityDid: COMMUNITY_DID, + appPassword: "new-correct-pw", + }); + expect(reauth.status).toBe(200); + + // getHealth still works with the new stored password. + const health = await call(app, "GET", `/xrpc/test.comm.community.getHealth?communityDid=${COMMUNITY_DID}`, ALICE); + expect(((await health.json()) as any).status).toBe("healthy"); + }); + + it("reauth requires owner in $admin", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.reauth", BOB, { + communityDid: COMMUNITY_DID, + appPassword: "correct-pw", + }); + expect(res.status).toBe(403); + }); + + it("reauth with bad credentials is rejected", async () => { + const res = await call(app, "POST", "/xrpc/test.comm.community.reauth", ALICE, { + communityDid: COMMUNITY_DID, + appPassword: "totally-wrong", + }); + expect(res.status).toBe(401); + }); +}); diff --git a/tests/spaces-acl.test.ts b/tests/spaces-acl.test.ts index 81ff32a..4f09ea5 100644 --- a/tests/spaces-acl.test.ts +++ b/tests/spaces-acl.test.ts @@ -9,7 +9,6 @@ function mkSpace(overrides: Partial = {}): SpaceRow { type: "tools.atmo.event.space", key: "s1", serviceDid: "did:web:example.com#svc", - memberListRef: null, appPolicyRef: null, appPolicy: null, createdAt: 1, @@ -18,8 +17,8 @@ function mkSpace(overrides: Partial = {}): SpaceRow { }; } -function mkMember(did: string, perms: "read" | "write" = "write"): SpaceMemberRow { - return { spaceUri: "x", did, perms, addedAt: 1, addedBy: null }; +function mkMember(did: string): SpaceMemberRow { + return { spaceUri: "x", did, addedAt: 1, addedBy: null }; } describe("spaces acl", () => { @@ -48,38 +47,17 @@ describe("spaces acl", () => { expect((r as any).reason).toBe("not-member"); }); - it("member with read perm can read", () => { + it("member can read and write (no perm tiering)", () => { const s = mkSpace(); - const r = checkAccess({ - op: "read", - space: s, - callerDid: "did:plc:bob", - member: mkMember("did:plc:bob", "read"), - }); - expect(r.allow).toBe(true); - }); - - it("member with read perm cannot write", () => { - const s = mkSpace(); - const r = checkAccess({ - op: "write", - space: s, - callerDid: "did:plc:bob", - member: mkMember("did:plc:bob", "read"), - }); - expect(r.allow).toBe(false); - expect((r as any).reason).toBe("not-writer"); - }); - - it("member with write perm can write", () => { - const s = mkSpace(); - const r = checkAccess({ - op: "write", - space: s, - callerDid: "did:plc:bob", - member: mkMember("did:plc:bob", "write"), - }); - expect(r.allow).toBe(true); + for (const op of ["read", "write"] as const) { + const r = checkAccess({ + op, + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob"), + }); + expect(r.allow).toBe(true); + } }); it("non-member cannot write", () => { @@ -91,28 +69,28 @@ describe("spaces acl", () => { member: null, }); expect(r.allow).toBe(false); - expect((r as any).reason).toBe("not-writer"); + expect((r as any).reason).toBe("not-member"); }); - it("delete own: member-with-write can delete own record", () => { + it("delete own: member can delete own record", () => { const s = mkSpace(); const r = checkAccess({ op: "delete", space: s, callerDid: "did:plc:bob", - member: mkMember("did:plc:bob", "write"), + member: mkMember("did:plc:bob"), targetAuthorDid: "did:plc:bob", }); expect(r.allow).toBe(true); }); - it("delete other's: member-with-write cannot delete someone else's record", () => { + it("delete other's: member cannot delete someone else's record", () => { const s = mkSpace(); const r = checkAccess({ op: "delete", space: s, callerDid: "did:plc:bob", - member: mkMember("did:plc:bob", "write"), + member: mkMember("did:plc:bob"), targetAuthorDid: "did:plc:charlie", }); expect(r.allow).toBe(false); @@ -131,17 +109,17 @@ describe("spaces acl", () => { expect(r.allow).toBe(true); }); - it("delete without write perm: read-only member cannot delete own", () => { + it("delete by non-member: denied as not-member, not not-own-record", () => { const s = mkSpace(); const r = checkAccess({ op: "delete", space: s, callerDid: "did:plc:bob", - member: mkMember("did:plc:bob", "read"), + member: null, targetAuthorDid: "did:plc:bob", }); expect(r.allow).toBe(false); - expect((r as any).reason).toBe("not-writer"); + expect((r as any).reason).toBe("not-member"); }); it("app policy: allow-mode with apps[] denylists those apps", () => { diff --git a/tests/spaces-e2e.test.ts b/tests/spaces-e2e.test.ts index 92b1fe5..1fd965c 100644 --- a/tests/spaces-e2e.test.ts +++ b/tests/spaces-e2e.test.ts @@ -136,7 +136,6 @@ describe("spaces e2e", () => { const res = await call(app, "POST", "/xrpc/test.spaces.space.addMember", ALICE, { spaceUri, did: BOB, - perms: "write", }); expect(res.status).toBe(200); }); diff --git a/tests/spaces-invites.test.ts b/tests/spaces-invites.test.ts index e14f641..22219b3 100644 --- a/tests/spaces-invites.test.ts +++ b/tests/spaces-invites.test.ts @@ -93,7 +93,6 @@ describe("invite e2e", () => { it("owner creates an invite and Bob redeems it to become a member", async () => { const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { spaceUri, - perms: "write", }); expect(create.status).toBe(200); const { token, invite } = (await create.json()) as any; @@ -106,7 +105,6 @@ describe("invite e2e", () => { expect(redeem.status).toBe(200); const body = (await redeem.json()) as any; expect(body.spaceUri).toBe(spaceUri); - expect(body.perms).toBe("write"); // Bob is now a member — can write a message const put = await call(app, "POST", "/xrpc/test.spaces.space.putRecord", BOB, { @@ -201,7 +199,7 @@ describe("invite e2e", () => { it("read-join token grants anonymous read AND can be redeemed for membership", async () => { const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { - spaceUri, kind: "read-join", perms: "write", + spaceUri, kind: "read-join", }); const { token, invite } = (await create.json()) as any; expect(invite.kind).toBe("read-join"); diff --git a/tests/types.test.ts b/tests/types.test.ts index b18d746..bec1de5 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -183,7 +183,7 @@ describe("resolveConfig", () => { it("applies default jetstreams and relays", () => { const resolved = resolveConfig({ namespace: "test", collections: {} }); - expect(resolved.jetstreams).toHaveLength(4); + expect(resolved.jetstreams).toHaveLength(1); expect(resolved.relays).toHaveLength(1); }); -- 2.51.2 From 33a6faaaad6c7c13e294d1df45c26264859f712c Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:31:14 +0200 Subject: [PATCH 02/18] some fixes --- docs/todo.md | 4 + .../sveltekit-cloudflare-workers/package.json | 8 +- .../rsvp/atmo/space/listSpaces.json | 7 +- pnpm-lock.yaml | 101 ++++++++---------- spaces-lexicon-templates/listSpaces.json | 3 +- src/core/community/acl.ts | 27 +++++ src/core/community/adapter.ts | 51 ++++----- src/core/community/router.ts | 24 ++++- src/core/community/types.ts | 9 +- src/core/spaces/router.ts | 6 +- tests/community-delegation.test.ts | 54 ++++++++++ tests/community-e2e.test.ts | 13 +++ tests/spaces-e2e.test.ts | 34 ++++++ 13 files changed, 240 insertions(+), 101 deletions(-) create mode 100644 docs/todo.md diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..6e2293a --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,4 @@ +- permissioned blobs +- websockets/sse/webhooks +- add ingestion logic? +- public spaces? \ No newline at end of file diff --git a/examples/sveltekit-cloudflare-workers/package.json b/examples/sveltekit-cloudflare-workers/package.json index f1d962c..f45b5ab 100644 --- a/examples/sveltekit-cloudflare-workers/package.json +++ b/examples/sveltekit-cloudflare-workers/package.json @@ -59,10 +59,10 @@ "license": "MIT", "dependencies": { "@atcute/jetstream": "^1.1.2", - "@atmo-dev/contrail": "^0.0.3", - "@foxui/core": "^0.8.2", - "@foxui/social": "^0.8.4", - "@foxui/time": "^0.8.2", + "@atmo-dev/contrail": "^0.1.1", + "@foxui/core": "^0.9.1", + "@foxui/social": "^0.8.10", + "@foxui/time": "^0.8.5", "valibot": "^1.3.1" } } diff --git a/lexicons-generated/rsvp/atmo/space/listSpaces.json b/lexicons-generated/rsvp/atmo/space/listSpaces.json index 39f2992..531ab35 100644 --- a/lexicons-generated/rsvp/atmo/space/listSpaces.json +++ b/lexicons-generated/rsvp/atmo/space/listSpaces.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns.", + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns. When scope='member', the optional 'owner' param narrows to spaces owned by that DID — useful for listing all channels in a specific community the caller can access.", "parameters": { "type": "params", "properties": { @@ -20,6 +20,11 @@ "type": "string", "format": "nsid" }, + "owner": { + "type": "string", + "format": "did", + "description": "With scope=member, filter to spaces owned by this DID. Ignored when scope=owner." + }, "cursor": { "type": "string" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af3fd0c..9ff7d67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,17 +76,17 @@ importers: specifier: ^1.1.2 version: 1.1.2 '@atmo-dev/contrail': - specifier: link:../.. - version: link:../.. + specifier: ^0.1.1 + version: 0.1.1(pg@8.20.0) '@foxui/core': - specifier: ^0.8.2 - version: 0.8.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + specifier: ^0.9.1 + version: 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) '@foxui/social': - specifier: ^0.8.4 - version: 0.8.6(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + specifier: ^0.8.10 + version: 0.8.10(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) '@foxui/time': - specifier: ^0.8.2 - version: 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + specifier: ^0.8.5 + version: 0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) valibot: specifier: ^1.3.1 version: 1.3.1(typescript@6.0.2) @@ -295,6 +295,14 @@ packages: '@atcute/xrpc-server@0.1.12': resolution: {integrity: sha512-70KIerQlljp5+s6t0u6YNN9klEboQUZa2hhoi/hmXIO1cIKEORettTMctnyjfcCJaSfAuj42dxPu51GTZBlm8w==} + '@atmo-dev/contrail@0.1.1': + resolution: {integrity: sha512-7BdLYjHHGC2EKlT/p3KszJAE6Pv1FPPuLVG+eYRpXSTF6iwrk/FVA0Dep7lTowbLlo5mQBpbEpo2CrQtUZAPfQ==} + peerDependencies: + pg: ^8.0.0 + peerDependenciesMeta: + pg: + optional: true + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -843,32 +851,26 @@ packages: '@fontsource-variable/geist@5.2.8': resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} - '@foxui/core@0.8.3': - resolution: {integrity: sha512-dQE+fhwDT4SlUhFFGs8+609S9rBC+gyY3Tjd48jIQtJai3hOAIVxFFB+j/ngNiTzMME3oDV7yc6H/OAghTy8Eg==} + '@foxui/core@0.9.1': + resolution: {integrity: sha512-GnMegqx5gz71U85FK85ayEEHuD3bvpNET8HeNsJU1Gj+OVkKKvUY9q/XqmCY3tsfEqY0Mv+RM1iQSnsZn5LZ6w==} peerDependencies: svelte: '>=5' tailwindcss: '>=3' - '@foxui/core@0.9.0': - resolution: {integrity: sha512-+zzya5pclKhyQYKdAAdpIGa6GG/oipvBi6PM/T/zynXmHEdSm6bWqHtlcL98Ulvp4cmML7VM9OtkvXVk4NeP/w==} + '@foxui/social@0.8.10': + resolution: {integrity: sha512-d9FB1BjWFbvldNlrXogdJU/a90dDdNqvBWGAkLRK2i7iiBAaWkKceWL3xc8N5+79+hfD8Ydg4ptCk3wx3bkguw==} peerDependencies: svelte: '>=5' tailwindcss: '>=3' - '@foxui/social@0.8.6': - resolution: {integrity: sha512-Hk78zsc8/1sHzQTBiy95Kvu05k7YQTLZeqRjk/L0bZsjMItNRik8ocheKHEK4f01L8ZaOmxnREVhf4P2nUm/1w==} + '@foxui/text@0.8.5': + resolution: {integrity: sha512-K7XsJGWdyRdXlRxjD8Z4TQ9xy7T/rVrB/z+U0YGGIjIER1DQGLawPwBQ5WXYQeNCBS1ab8mG67uFZw2sA5aFPQ==} peerDependencies: svelte: '>=5' tailwindcss: '>=3' - '@foxui/text@0.8.4': - resolution: {integrity: sha512-eW5EJE9BSgJa1kx9u95960OTIKcVsO6+nFidpdvdBak1fMoIlZTb/HL2nlPG21HbWewBIys7th0SC5B4wMWfxw==} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/time@0.8.4': - resolution: {integrity: sha512-HOa2DqHtOMSd5JDv9XZ+AVFnmJ5Oi1wnK+dCfbd/+2LXhuQKqg5kQcclyX9Ufhprwuhx59JO7JUe4Vix8jBmpw==} + '@foxui/time@0.8.5': + resolution: {integrity: sha512-InXhgKZW4URArJWW87hblhHMAoNii0iueKCxei72SCvlABZ/y5QoX/ewhrgk7/47qbfrlgyqBzshDtF60kTqcg==} peerDependencies: svelte: '>=5' tailwindcss: '>=3' @@ -3761,12 +3763,25 @@ snapshots: '@atcute/crypto': 2.4.1 '@atcute/identity': 1.1.4 '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 '@badrap/valita': 0.4.6 nanoid: 5.1.7 + '@atmo-dev/contrail@0.1.1(pg@8.20.0)': + dependencies: + '@atcute/atproto': 3.1.11 + '@atcute/client': 4.2.1 + '@atcute/identity': 1.1.4 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/jetstream': 1.1.2 + '@atcute/lexicons': 1.3.0 + '@atcute/xrpc-server': 0.1.12 + hono: 4.12.8 + optionalDependencies: + pg: 8.20.0 + '@babel/runtime@7.29.2': {} '@badrap/valita@0.4.6': {} @@ -4209,29 +4224,7 @@ snapshots: '@fontsource-variable/geist@5.2.8': {} - '@foxui/core@0.8.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': - dependencies: - '@fontsource-variable/geist': 5.2.8 - '@fontsource-variable/geist-mono': 5.2.7 - '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - clsx: 2.1.1 - dompurify: 3.4.0 - mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - tailwind-merge: 3.5.0 - tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) - tailwindcss: 4.2.2 - web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - react - - react-dom - - vue - - '@foxui/core@0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + '@foxui/core@0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': dependencies: '@fontsource-variable/geist': 5.2.8 '@fontsource-variable/geist-mono': 5.2.7 @@ -4255,13 +4248,13 @@ snapshots: - react-dom - vue - '@foxui/social@0.8.6(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + '@foxui/social@0.8.10(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': dependencies: '@atcute/bluesky': 3.3.2 '@atcute/bluesky-richtext-segmenter': 3.0.0 - '@foxui/core': 0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@foxui/text': 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@foxui/time': 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/core': 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/text': 0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/time': 0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) '@use-gesture/vanilla': 10.3.1 bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) emoji-picker-element: 1.29.1 @@ -4280,10 +4273,10 @@ snapshots: - react-dom - vue - '@foxui/text@0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + '@foxui/text@0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': dependencies: '@floating-ui/dom': 1.7.6 - '@foxui/core': 0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/core': 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) '@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) '@tiptap/extension-code-block-lowlight': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(highlight.js@11.11.1)(lowlight@3.3.0) @@ -4319,9 +4312,9 @@ snapshots: - react-dom - vue - '@foxui/time@0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + '@foxui/time@0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': dependencies: - '@foxui/core': 0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/core': 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) svelte: 5.55.4(@typescript-eslint/types@8.58.2) diff --git a/spaces-lexicon-templates/listSpaces.json b/spaces-lexicon-templates/listSpaces.json index bc7a306..4a70b9d 100644 --- a/spaces-lexicon-templates/listSpaces.json +++ b/spaces-lexicon-templates/listSpaces.json @@ -4,12 +4,13 @@ "defs": { "main": { "type": "query", - "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns.", + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns. When scope='member', the optional 'owner' param narrows to spaces owned by that DID — useful for listing all channels in a specific community the caller can access.", "parameters": { "type": "params", "properties": { "scope": { "type": "string", "knownValues": ["member", "owner"], "default": "member" }, "type": { "type": "string", "format": "nsid" }, + "owner": { "type": "string", "format": "did", "description": "With scope=member, filter to spaces owned by this DID. Ignored when scope=owner." }, "cursor": { "type": "string" }, "limit": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } } diff --git a/src/core/community/acl.ts b/src/core/community/acl.ts index b043f30..5ef23a2 100644 --- a/src/core/community/acl.ts +++ b/src/core/community/acl.ts @@ -79,6 +79,33 @@ export async function flattenEffectiveMembers( return dids; } +/** All space URIs `actorDid` can reach (directly or via delegation). + * Walks the reverse graph: direct grants first, then every space that + * delegates to an already-reachable space. Used for community.list. */ +export async function resolveReachableSpaces( + adapter: CommunityAdapter, + actorDid: string, + opts: EffectiveLevelOptions = {} +): Promise> { + const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH; + const reachable = new Set(); + const directRows = await adapter.listAccessRowsForSubject(actorDid); + const queue: Array<{ spaceUri: string; depth: number }> = directRows.map( + (r) => ({ spaceUri: r.spaceUri, depth: 0 }) + ); + while (queue.length) { + const { spaceUri, depth } = queue.shift()!; + if (reachable.has(spaceUri)) continue; + reachable.add(spaceUri); + if (depth >= maxDepth) continue; + const parents = await adapter.listSpacesDelegatingTo(spaceUri); + for (const parent of parents) { + if (!reachable.has(parent)) queue.push({ spaceUri: parent, depth: depth + 1 }); + } + } + return reachable; +} + /** Check whether the actor would cause a cycle if added as a subject-space of `spaceUri`. */ export async function wouldCycle( adapter: CommunityAdapter, diff --git a/src/core/community/adapter.ts b/src/core/community/adapter.ts index 8ef88c3..ae9e62a 100644 --- a/src/core/community/adapter.ts +++ b/src/core/community/adapter.ts @@ -10,32 +10,13 @@ function mapCommunityRow(row: any): CommunityRow { did: row.did, mode: row.mode as CommunityMode, pdsEndpoint: row.pds_endpoint ?? null, - appPasswordEncrypted: row.app_password_encrypted - ? toBytes(row.app_password_encrypted) - : null, identifier: row.identifier ?? null, - signingKeyEncrypted: row.signing_key_encrypted - ? toBytes(row.signing_key_encrypted) - : null, - rotationKeyEncrypted: row.rotation_key_encrypted - ? toBytes(row.rotation_key_encrypted) - : null, createdBy: row.created_by, createdAt: toNum(row.created_at), deletedAt: row.deleted_at == null ? null : toNum(row.deleted_at), }; } -function toBytes(v: unknown): Uint8Array { - // Encrypted fields are stored base64 as TEXT; passthrough for other code paths. - if (v instanceof Uint8Array) return v; - if (typeof v === "string") { - // Treat as base64 for storage. Caller's cipher handles further decoding. - return new TextEncoder().encode(v); - } - throw new Error("unexpected blob column type"); -} - function mapAccessRow(row: any): AccessLevelRow { const isSpace = row.subject_kind === "space"; return { @@ -96,10 +77,7 @@ export class CommunityAdapter { did: input.did, mode: "adopt", pdsEndpoint: input.pdsEndpoint, - appPasswordEncrypted: new TextEncoder().encode(input.appPasswordEncrypted), identifier: input.identifier, - signingKeyEncrypted: null, - rotationKeyEncrypted: null, createdBy: input.createdBy, createdAt: now, deletedAt: null, @@ -125,10 +103,7 @@ export class CommunityAdapter { did: input.did, mode: "mint", pdsEndpoint: null, - appPasswordEncrypted: null, identifier: null, - signingKeyEncrypted: new TextEncoder().encode(input.signingKeyEncrypted), - rotationKeyEncrypted: new TextEncoder().encode(input.rotationKeyEncrypted), createdBy: input.createdBy, createdAt: now, deletedAt: null, @@ -187,19 +162,31 @@ export class CommunityAdapter { .run(); } - async listCommunitiesForActor(actor: string): Promise { - // Communities where the actor has any access level in any community-owned space. + /** Direct access-level rows held by the given DID subject. Used as the seed + * set for reverse-graph traversal. */ + async listAccessRowsForSubject(subjectDid: string): Promise { + const { results } = await this.db + .prepare( + `SELECT * FROM community_access_levels + WHERE subject_kind = 'did' AND subject = ?` + ) + .bind(subjectDid) + .all(); + return results.map(mapAccessRow); + } + + /** Communities owning any of the given space URIs. */ + async listCommunitiesOwningSpaces(spaceUris: string[]): Promise { + if (spaceUris.length === 0) return []; + const placeholders = spaceUris.map(() => "?").join(","); const { results } = await this.db .prepare( `SELECT DISTINCT c.* FROM communities c JOIN spaces s ON s.owner_did = c.did AND s.deleted_at IS NULL - JOIN community_access_levels cal - ON cal.space_uri = s.uri - AND cal.subject_kind = 'did' AND cal.subject = ? - WHERE c.deleted_at IS NULL + WHERE c.deleted_at IS NULL AND s.uri IN (${placeholders}) ORDER BY c.created_at DESC` ) - .bind(actor) + .bind(...spaceUris) .all(); return results.map(mapCommunityRow); } diff --git a/src/core/community/router.ts b/src/core/community/router.ts index 07be342..c46acc5 100644 --- a/src/core/community/router.ts +++ b/src/core/community/router.ts @@ -14,7 +14,7 @@ import { computeDidPlc, submitGenesisOp, } from "./plc"; -import { resolveEffectiveLevel, wouldCycle } from "./acl"; +import { resolveEffectiveLevel, resolveReachableSpaces, wouldCycle } from "./acl"; import { reconcile } from "./reconcile"; import type { AccessLevel } from "./types"; import { @@ -239,7 +239,10 @@ export function registerCommunityRoutes( app.get(`/xrpc/${NS}.list`, auth, async (c) => { const sa = getAuth(c); const actor = c.req.query("actor") ?? sa.issuer; - const rows = await community.listCommunitiesForActor(actor); + // Walk the delegation graph so communities reached via group memberships + // (not just direct DID grants) are included. + const reachable = await resolveReachableSpaces(community, actor); + const rows = await community.listCommunitiesOwningSpaces([...reachable]); return c.json({ communities: rows.map((r) => ({ did: r.did, @@ -363,6 +366,12 @@ export function registerCommunityRoutes( return c.json({ error: "Forbidden", reason: "owner-or-admin-required" }, 403); } + // Before clearing this space's ACL rows, capture any spaces that delegate + // to it so we can re-reconcile their materialized membership after the + // delete — any DIDs that reached those parents only through this space + // must be removed from their spaces_members. + const delegatingParents = await community.listSpacesDelegatingTo(body.spaceUri); + await spaces.deleteSpace(body.spaceUri); await community.deleteAllAccessForSpace(body.spaceUri); // Drop the materialized membership too. @@ -375,6 +384,9 @@ export function registerCommunityRoutes( sa.issuer ); } + for (const parent of delegatingParents) { + await reconcile(community, spaces, parent, sa.issuer); + } return c.json({ ok: true }); }); @@ -429,6 +441,14 @@ export function registerCommunityRoutes( if (rankOf(body.accessLevel) > rankOf(callerLevel)) { return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); } + // grant() upserts; block downgrading a subject who already outranks the caller. + const existingGrant = await community.getAccessRow( + body.spaceUri, + (subjectDid ?? subjectSpaceUri)! + ); + if (existingGrant && rankOf(existingGrant.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-modify-higher-than-self" }, 403); + } // Cycle check when delegating to another space. if (subjectSpaceUri) { diff --git a/src/core/community/types.ts b/src/core/community/types.ts index 5b7bfe1..9cf690f 100644 --- a/src/core/community/types.ts +++ b/src/core/community/types.ts @@ -37,17 +37,14 @@ export interface CommunityConfig { fetch?: typeof fetch; } +/** Public view of a community row. Encrypted credentials are not included here + * — those live only behind CommunityAdapter.getRawCredentials(), which returns + * the base64 envelope string intended to be passed straight to CredentialCipher. */ export interface CommunityRow { did: string; mode: CommunityMode; - // adopt-mode fields pdsEndpoint: string | null; - appPasswordEncrypted: Uint8Array | null; identifier: string | null; - // mint-mode fields (Stage 4) - signingKeyEncrypted: Uint8Array | null; - rotationKeyEncrypted: Uint8Array | null; - // common createdBy: string; createdAt: number; deletedAt: number | null; diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index 0b2e188..c1c102c 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -83,12 +83,16 @@ export function registerSpacesRoutes( const sa = getAuth(c); const scope = c.req.query("scope") ?? "member"; // "member" | "owner" const type = c.req.query("type") ?? undefined; + const owner = c.req.query("owner") ?? undefined; const cursor = c.req.query("cursor") ?? undefined; const limit = c.req.query("limit") ? Number(c.req.query("limit")) : undefined; const opts: Parameters[0] = { type, cursor, limit }; if (scope === "owner") opts.ownerDid = sa.issuer; - else opts.memberDid = sa.issuer; + else { + opts.memberDid = sa.issuer; + if (owner) opts.ownerDid = owner; // narrow to spaces owned by this DID + } const result = await adapter.listSpaces(opts); return c.json({ diff --git a/tests/community-delegation.test.ts b/tests/community-delegation.test.ts index e6d366b..7466dac 100644 --- a/tests/community-delegation.test.ts +++ b/tests/community-delegation.test.ts @@ -25,6 +25,7 @@ const CONFIG: ContrailConfig = { }, community: { masterKey: MASTER_KEY, + plcDirectory: "https://plc.test", fetch: mockFetch, resolver: mockResolver(), }, @@ -56,6 +57,10 @@ async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise< { status: 200, headers: { "content-type": "application/json" } } ); } + // PLC directory submissions for community.mint. + if (url.startsWith("https://plc.test/")) { + return new Response("{}", { status: 200 }); + } return new Response("not found", { status: 404 }); } @@ -225,6 +230,24 @@ describe("community delegation — stage 2", () => { expect(await flatMembers(app, ALICE, chat)).not.toContain(DIANA); }); + it("deleting a delegated-from space re-reconciles parents' spaces_members", async () => { + const mods = await createSpace(app, ALICE, "cascade-mods"); + const chat = await createSpace(app, ALICE, "cascade-chat"); + + // Diana reaches chat only via mods. + expect((await grant(app, ALICE, mods, { did: DIANA }, "member")).status).toBe(200); + expect((await grant(app, ALICE, chat, { spaceUri: mods }, "member")).status).toBe(200); + expect(await flatMembers(app, ALICE, chat)).toContain(DIANA); + + // Delete mods; chat should no longer list Diana as a flattened member. + const del = await call(app, "POST", "/xrpc/test.comm.community.space.delete", ALICE, { + spaceUri: mods, + }); + expect(del.status).toBe(200); + + expect(await flatMembers(app, ALICE, chat)).not.toContain(DIANA); + }); + it("resync endpoint requires admin+", async () => { const s = await createSpace(app, ALICE, "resync-space"); // Bob is a plain member @@ -236,6 +259,37 @@ describe("community delegation — stage 2", () => { expect(res.status).toBe(403); }); + it("community.list follows delegation across communities", async () => { + // Mint a second community. DIANA will have a direct grant only in the + // second community's space; the second community delegates a group from + // the first community into one of its own spaces. The actor Alice has no + // direct grant in the second community but reaches it via the delegation + // chain, so community.list must surface both. + const mint = await call(app, "POST", "/xrpc/test.comm.community.mint", DIANA, {}); + expect(mint.status).toBe(200); + const second = ((await mint.json()) as any).communityDid as string; + + // Diana creates a space in the second community and delegates the first + // community's (Alice-owned) $admin in as a member. + const createRes = await call(app, "POST", "/xrpc/test.comm.community.space.create", DIANA, { + communityDid: second, + key: "bridged", + }); + expect(createRes.status).toBe(200); + const bridged = ((await createRes.json()) as any).space.uri as string; + + const firstAdmin = `at://${COMMUNITY_DID}/tools.atmo.event.space/$admin`; + expect((await grant(app, DIANA, bridged, { spaceUri: firstAdmin }, "member")).status).toBe(200); + + // Alice has no direct grant in the second community, but is reachable via + // `bridged` → firstAdmin (where she is owner). + const res = await call(app, "GET", `/xrpc/test.comm.community.list`, ALICE); + expect(res.status).toBe(200); + const dids = ((await res.json()) as any).communities.map((c: any) => c.did); + expect(dids).toContain(COMMUNITY_DID); + expect(dids).toContain(second); + }); + it("resync endpoint works for owner", async () => { const s = await createSpace(app, ALICE, "resync-ok"); const res = await call(app, "POST", "/xrpc/test.comm.community.space.resync", ALICE, { diff --git a/tests/community-e2e.test.ts b/tests/community-e2e.test.ts index 3acb71e..5203871 100644 --- a/tests/community-e2e.test.ts +++ b/tests/community-e2e.test.ts @@ -231,6 +231,19 @@ describe("community e2e — stage 1", () => { expect(((await res.json()) as any).reason).toBe("cannot-grant-higher-than-self"); }); + it("grant cannot downgrade a subject who outranks the caller", async () => { + const spaceUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/general`; + // Bob is currently manager (promoted earlier in this describe block). + // Alice is owner of the space. Bob tries to downgrade Alice to member via grant. + const res = await call(app, "POST", "/xrpc/test.comm.community.space.grant", BOB, { + spaceUri, + subject: { did: ALICE }, + accessLevel: "member", + }); + expect(res.status).toBe(403); + expect(((await res.json()) as any).reason).toBe("cannot-modify-higher-than-self"); + }); + it("revokes and reconciler removes from spaces_members", async () => { const spaceUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/general`; const res = await call(app, "POST", "/xrpc/test.comm.community.space.revoke", ALICE, { diff --git a/tests/spaces-e2e.test.ts b/tests/spaces-e2e.test.ts index 1fd965c..3989fa8 100644 --- a/tests/spaces-e2e.test.ts +++ b/tests/spaces-e2e.test.ts @@ -272,4 +272,38 @@ describe("spaces e2e", () => { const body = await asJson(res); expect(body.record.record.address).toBe("123 Main St"); }); + + it("listSpaces?owner= narrows scope=member to spaces owned by that DID", async () => { + // Alice creates a second space she owns. + await call(app, "POST", "/xrpc/test.spaces.space.createSpace", ALICE, { + key: "other-space", + }); + // Charlie owns yet another space and adds Bob as a member. + const charlieSpaceRes = await call(app, "POST", "/xrpc/test.spaces.space.createSpace", CHARLIE, { + key: "charlie-space", + }); + const charlieSpace = (await charlieSpaceRes.json() as any).space.uri; + await call(app, "POST", "/xrpc/test.spaces.space.addMember", CHARLIE, { + spaceUri: charlieSpace, + did: BOB, + }); + + // Without owner filter: Bob sees spaces from both Alice and Charlie. + const allRes = await call(app, "GET", "/xrpc/test.spaces.space.listSpaces?scope=member", BOB); + const all = (await allRes.json() as any).spaces; + const allOwners = new Set(all.map((s: any) => s.ownerDid)); + expect(allOwners.has(ALICE)).toBe(true); + expect(allOwners.has(CHARLIE)).toBe(true); + + // With owner=Alice: Bob sees only Alice-owned spaces. + const narrowedRes = await call( + app, + "GET", + `/xrpc/test.spaces.space.listSpaces?scope=member&owner=${ALICE}`, + BOB + ); + const narrowed = (await narrowedRes.json() as any).spaces; + expect(narrowed.length).toBeGreaterThan(0); + for (const s of narrowed) expect(s.ownerDid).toBe(ALICE); + }); }); -- 2.51.2 From aec16dea404fbccb0715eec6524111c3138426e4 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:47:18 +0200 Subject: [PATCH 03/18] pubsub --- package.json | 7 +- pnpm-lock.yaml | 3432 +-------------------- realtime-lexicon-templates/subscribe.json | 24 + realtime-lexicon-templates/ticket.json | 40 + src/core/realtime/durable-object.ts | 294 ++ src/core/realtime/in-memory.ts | 116 + src/core/realtime/index.ts | 37 + src/core/realtime/merge.ts | 77 + src/core/realtime/publishing-adapter.ts | 139 + src/core/realtime/resolve.ts | 90 + src/core/realtime/router.ts | 221 ++ src/core/realtime/sse.ts | 98 + src/core/realtime/ticket.ts | 159 + src/core/realtime/types.ts | 95 + src/core/realtime/websocket.ts | 84 + src/core/router/index.ts | 55 +- src/core/types.ts | 3 + src/generate.ts | 53 + src/index.ts | 23 + tests/realtime-durable-object.test.ts | 238 ++ tests/realtime-e2e.test.ts | 305 ++ tests/realtime-foundations.test.ts | 144 + 22 files changed, 2322 insertions(+), 3412 deletions(-) create mode 100644 realtime-lexicon-templates/subscribe.json create mode 100644 realtime-lexicon-templates/ticket.json create mode 100644 src/core/realtime/durable-object.ts create mode 100644 src/core/realtime/in-memory.ts create mode 100644 src/core/realtime/index.ts create mode 100644 src/core/realtime/merge.ts create mode 100644 src/core/realtime/publishing-adapter.ts create mode 100644 src/core/realtime/resolve.ts create mode 100644 src/core/realtime/router.ts create mode 100644 src/core/realtime/sse.ts create mode 100644 src/core/realtime/ticket.ts create mode 100644 src/core/realtime/types.ts create mode 100644 src/core/realtime/websocket.ts create mode 100644 tests/realtime-durable-object.test.ts create mode 100644 tests/realtime-e2e.test.ts create mode 100644 tests/realtime-foundations.test.ts diff --git a/package.json b/package.json index a17637d..17c62d7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "files": [ "dist", "spaces-lexicon-templates/**/*.json", - "community-lexicon-templates/**/*.json" + "community-lexicon-templates/**/*.json", + "realtime-lexicon-templates/**/*.json" ], "exports": { ".": { @@ -35,7 +36,8 @@ "import": "./dist/publish.js" }, "./spaces-lexicon-templates/*.json": "./spaces-lexicon-templates/*.json", - "./community-lexicon-templates/*.json": "./community-lexicon-templates/*.json" + "./community-lexicon-templates/*.json": "./community-lexicon-templates/*.json", + "./realtime-lexicon-templates/*.json": "./realtime-lexicon-templates/*.json" }, "repository": { "type": "git", @@ -67,6 +69,7 @@ }, "dependencies": { "@atcute/atproto": "^3.1.10", + "@atcute/cid": "^2.4.1", "@atcute/client": "^4.2.1", "@atcute/identity": "^1.1.4", "@atcute/identity-resolver": "^1.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ff7d67..598af1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@atcute/atproto': specifier: ^3.1.10 version: 3.1.10 + '@atcute/cid': + specifier: ^2.4.1 + version: 2.4.1 '@atcute/client': specifier: ^4.2.1 version: 4.2.1 @@ -56,7 +59,7 @@ importers: version: 8.20.0 tsup: specifier: ^8.5.0 - version: 8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -65,146 +68,16 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0)) + version: 4.1.0(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)) wrangler: specifier: ^4.63.0 version: 4.63.0(@cloudflare/workers-types@4.20260205.0) - examples/sveltekit-cloudflare-workers: - dependencies: - '@atcute/jetstream': - specifier: ^1.1.2 - version: 1.1.2 - '@atmo-dev/contrail': - specifier: ^0.1.1 - version: 0.1.1(pg@8.20.0) - '@foxui/core': - specifier: ^0.9.1 - version: 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@foxui/social': - specifier: ^0.8.10 - version: 0.8.10(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@foxui/time': - specifier: ^0.8.5 - version: 0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - valibot: - specifier: ^1.3.1 - version: 1.3.1(typescript@6.0.2) - devDependencies: - '@atcute/atproto': - specifier: ^3.1.10 - version: 3.1.10 - '@atcute/bluesky': - specifier: ^3.3.0 - version: 3.3.2 - '@atcute/client': - specifier: ^4.2.1 - version: 4.2.1 - '@atcute/identity-resolver': - specifier: ^1.2.2 - version: 1.2.2(@atcute/identity@1.1.4) - '@atcute/lex-cli': - specifier: ^2.5.3 - version: 2.5.3 - '@atcute/lexicon-doc': - specifier: ^2.1.2 - version: 2.1.2 - '@atcute/lexicons': - specifier: ^1.2.9 - version: 1.2.9 - '@atcute/oauth-node-client': - specifier: ^1.1.0 - version: 1.1.0 - '@atcute/tid': - specifier: ^1.1.2 - version: 1.1.2 - '@cloudflare/workers-types': - specifier: ^4.20260317.1 - version: 4.20260414.1 - '@eslint/compat': - specifier: ^2.0.3 - version: 2.0.5(eslint@10.2.0(jiti@2.6.1)) - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.2.0(jiti@2.6.1)) - '@sveltejs/adapter-cloudflare': - specifier: ^7.2.8 - version: 7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.82.2(@cloudflare/workers-types@4.20260414.1)) - '@sveltejs/kit': - specifier: ^2.55.0 - version: 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - '@sveltejs/vite-plugin-svelte': - specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - '@tailwindcss/forms': - specifier: ^0.5.11 - version: 0.5.11(tailwindcss@4.2.2) - '@tailwindcss/vite': - specifier: ^4.2.2 - version: 4.2.2(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - bits-ui: - specifier: ^2.16.4 - version: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - eslint: - specifier: ^10.1.0 - version: 10.2.0(jiti@2.6.1) - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-svelte: - specifier: ^3.16.0 - version: 3.17.0(eslint@10.2.0(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - globals: - specifier: ^17.4.0 - version: 17.5.0 - prettier: - specifier: ^3.8.1 - version: 3.8.1 - prettier-plugin-svelte: - specifier: ^3.5.1 - version: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - prettier-plugin-tailwindcss: - specifier: ^0.7.2 - version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)))(prettier@3.8.1) - svelte: - specifier: ^5.55.0 - version: 5.55.4(@typescript-eslint/types@8.58.2) - svelte-check: - specifier: ^4.4.5 - version: 4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2) - tailwindcss: - specifier: ^4.2.2 - version: 4.2.2 - tsx: - specifier: ^4.21.0 - version: 4.21.0 - typescript: - specifier: ^6.0.2 - version: 6.0.2 - typescript-eslint: - specifier: ^8.57.2 - version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - vite: - specifier: ^8.0.3 - version: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - wrangler: - specifier: ^4.78.0 - version: 4.82.2(@cloudflare/workers-types@4.20260414.1) - packages: '@atcute/atproto@3.1.10': resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} - '@atcute/atproto@3.1.11': - resolution: {integrity: sha512-yh+ASvA+iHHQij6UeHEKp2+rwvFvQR8A6/5Dk/xvqDslIikWEFx9VlprNwm/clQIPl2bLuQg+LHS8uY9o5nFTA==} - - '@atcute/bluesky-richtext-segmenter@3.0.0': - resolution: {integrity: sha512-NhZTUKtFpeBBbILwAcxj5u4RobIoHOmGw3CAaaEFNebKYSvmTecrXJ7XufHw5DFOUdr8SiKXQVRQxGAxulMNWg==} - - '@atcute/bluesky@3.3.2': - resolution: {integrity: sha512-BqVXghoNFbBm38hveWqPQQ25VZxHXmqYk5h0NG8YFiwNLrI8Ww1nXHAJzS65zyHwV5QeSA6EDFaTrQwm+7cCFQ==} - '@atcute/car@5.1.1': resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} @@ -256,27 +129,9 @@ packages: '@atcute/multibase@1.2.0': resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} - '@atcute/oauth-crypto@0.1.0': - resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==} - - '@atcute/oauth-keyset@0.1.0': - resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==} - - '@atcute/oauth-node-client@1.1.0': - resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==} - - '@atcute/oauth-types@0.1.1': - resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} - '@atcute/repo@0.1.3': resolution: {integrity: sha512-kN4gkrkQgJwI5xkVQ9zSdI8ULR47uH48EdJdjMvKAWsSDu8zKYd8ZQ6n778qa6o3WYEq02v9QVf7pzeU+3fYVA==} - '@atcute/tid@1.1.2': - resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} - - '@atcute/time-ms@1.3.2': - resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==} - '@atcute/uint8array@1.1.1': resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==} @@ -295,14 +150,6 @@ packages: '@atcute/xrpc-server@0.1.12': resolution: {integrity: sha512-70KIerQlljp5+s6t0u6YNN9klEboQUZa2hhoi/hmXIO1cIKEORettTMctnyjfcCJaSfAuj42dxPu51GTZBlm8w==} - '@atmo-dev/contrail@0.1.1': - resolution: {integrity: sha512-7BdLYjHHGC2EKlT/p3KszJAE6Pv1FPPuLVG+eYRpXSTF6iwrk/FVA0Dep7lTowbLlo5mQBpbEpo2CrQtUZAPfQ==} - peerDependencies: - pg: ^8.0.0 - peerDependenciesMeta: - pg: - optional: true - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -379,88 +226,43 @@ packages: workerd: optional: true - '@cloudflare/unenv-preset@2.16.0': - resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 - peerDependenciesMeta: - workerd: - optional: true - '@cloudflare/workerd-darwin-64@1.20260205.0': resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260410.1': - resolution: {integrity: sha512-0sh6xPmCKUfv/lUklP1dfyeKxCuEZGS0HeduxnucL8ECxSgAdWTOD42h/lQTwZCIiWtyHB+ZNB9hsS2Mlf0tMQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260205.0': resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260410.1': - resolution: {integrity: sha512-r2On29gPvlk/eiH/OpeUT23xoB8W8D1PHr8lul5nyxElLqvh3yNxZUnJWrbcOl+ubfrvw7+jFwgopMe17xyf0g==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - '@cloudflare/workerd-linux-64@1.20260205.0': resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260410.1': - resolution: {integrity: sha512-qWORRcAzPZeHJjrcYBNZTN6Y9l+iZQUz4KBdWbNrM6My4CpNrXS5kErPR373vG//5QPaDGwMXgBqyn9xfzarJQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260205.0': resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260410.1': - resolution: {integrity: sha512-jQfuHL4mnGDFyomSS3JNs9TpTvCu6Vzz2QSNCfJRstMzTICUFLMc4Vp/xKK+M5xkb0PoAu/G0hHx7jrxB2j+OQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - '@cloudflare/workerd-windows-64@1.20260205.0': resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260410.1': - resolution: {integrity: sha512-h8q/nbheDqpknY7AAOz19MuQkZAR1/bnoZnKipyeUPXt5No+y6HlTtva9Bohx5Fhc1MW2CX2MQVdb55qtkkqZQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - '@cloudflare/workers-types@4.20260205.0': resolution: {integrity: sha512-LTnpvcodmiuMwxmbrO2Fd0+Avbm2UVLLJxT8J2pRWPfoM44gmbIecXwOPZmDAMeadKWrBsQ+B0sloQAhUu5fpA==} - '@cloudflare/workers-types@4.20260414.1': - resolution: {integrity: sha512-E2wgYT1ywoM1M68nmVpxKdKzXsZm5vOu2plsqUixlK7YIydqsw31dZ+EjwXnAsdEjLaYC6XfsJayil8AEhyaBQ==} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.9.0': - resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} - '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} @@ -470,9 +272,6 @@ packages: '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -482,415 +281,156 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/android-arm64@0.27.0': resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm@0.27.0': resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-x64@0.27.0': resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/darwin-arm64@0.27.0': resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-x64@0.27.0': resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/freebsd-arm64@0.27.0': resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/linux-arm64@0.27.0': resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm@0.27.0': resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-ia32@0.27.0': resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-loong64@0.27.0': resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-mips64el@0.27.0': resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-ppc64@0.27.0': resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-riscv64@0.27.0': resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-s390x@0.27.0': resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-x64@0.27.0': resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/netbsd-arm64@0.27.0': resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/openbsd-arm64@0.27.0': resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/sunos-x64@0.27.0': resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/win32-arm64@0.27.0': resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-ia32@0.27.0': resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-x64@0.27.0': resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/compat@2.0.5': - resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^8.40 || 9 || 10 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/config-array@0.23.5': - resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.5.5': - resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.2.1': - resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.5': - resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.1': - resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - - '@fontsource-variable/geist-mono@5.2.7': - resolution: {integrity: sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA==} - - '@fontsource-variable/geist@5.2.8': - resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} - - '@foxui/core@0.9.1': - resolution: {integrity: sha512-GnMegqx5gz71U85FK85ayEEHuD3bvpNET8HeNsJU1Gj+OVkKKvUY9q/XqmCY3tsfEqY0Mv+RM1iQSnsZn5LZ6w==} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/social@0.8.10': - resolution: {integrity: sha512-d9FB1BjWFbvldNlrXogdJU/a90dDdNqvBWGAkLRK2i7iiBAaWkKceWL3xc8N5+79+hfD8Ydg4ptCk3wx3bkguw==} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/text@0.8.5': - resolution: {integrity: sha512-K7XsJGWdyRdXlRxjD8Z4TQ9xy7T/rVrB/z+U0YGGIjIER1DQGLawPwBQ5WXYQeNCBS1ab8mG67uFZw2sA5aFPQ==} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/time@0.8.5': - resolution: {integrity: sha512-InXhgKZW4URArJWW87hblhHMAoNii0iueKCxei72SCvlABZ/y5QoX/ewhrgk7/47qbfrlgyqBzshDtF60kTqcg==} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -1037,20 +577,9 @@ packages: '@types/node': optional: true - '@internationalized/date@3.12.0': - resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} - - '@jis3r/icons@2.7.0': - resolution: {integrity: sha512-RXee+W1AoWFHtFliLZ7SQxVopAezmCH0xU45HKTvvpDQ+6Y3g6tUHvKOK16J52TM8YsN4WDnsiOmcwau+bHmwg==} - peerDependencies: - svelte: ^5.0.0 - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1064,11 +593,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@lucide/svelte@1.8.0': - resolution: {integrity: sha512-+zYQUKqEOVP5lxbGmxL1OVgGMQtRK91eIJ0bR+3Cr1ts4oQEsQfxyzzd5X47psJlblAuGFrl2xm4YuATjR9oaA==} - peerDependencies: - svelte: ^5 - '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1081,9 +605,6 @@ packages: '@mary-ext/simple-event-emitter@1.0.0': resolution: {integrity: sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg==} - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - '@napi-rs/wasm-runtime@1.1.3': resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: @@ -1105,11 +626,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@number-flow/svelte@0.4.0': - resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==} - peerDependencies: - svelte: ^4 || ^5 - '@optique/core@0.6.11': resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} @@ -1118,19 +634,9 @@ packages: resolution: {integrity: sha512-tsXBEygGSzNpFK2gjsRlXBn7FiScUeLFWIZNpoAZ8iG85Km0/3K9xgqlQAXoQ+uEZBe4XplnzyCDvmEgbyNT8w==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} - '@oxc-project/runtime@0.115.0': - resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} - engines: {node: ^20.19.0 || >=22.12.0} - - '@oxc-project/types@0.115.0': - resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} - '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -1140,113 +646,56 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@remirror/core-constants@3.0.0': - resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@rolldown/binding-android-arm64@1.0.0-rc.15': resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -1257,76 +706,38 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': - resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/pluginutils@1.0.0-rc.15': resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} - '@rolldown/pluginutils@1.0.0-rc.9': - resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} - '@rollup/rollup-android-arm-eabi@4.60.1': resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} cpu: [arm] @@ -1462,351 +873,18 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} - peerDependencies: - acorn: ^8.9.0 - - '@sveltejs/adapter-cloudflare@7.2.8': - resolution: {integrity: sha512-bIdhY/Fi4AQmqiBdQVKnafH1h9Gw+xbCvHyUu4EouC8rJOU02zwhi14k/FDhQ0mJF1iblIu3m8UNQ8GpGIvIOQ==} - peerDependencies: - '@sveltejs/kit': ^2.0.0 - wrangler: ^4.0.0 - - '@sveltejs/kit@2.57.1': - resolution: {integrity: sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==} - engines: {node: '>=18.13'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.0.0 - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 || ^6.0.0 - vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - typescript: - optional: true - - '@sveltejs/vite-plugin-svelte@7.0.0': - resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.46.4 - vite: ^8.0.0-beta.7 || ^8.0.0 - - '@swc/helpers@0.5.21': - resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - - '@tailwindcss/forms@0.5.11': - resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} - peerDependencies: - tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - - '@tailwindcss/node@4.2.2': - resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} - - '@tailwindcss/oxide-android-arm64@4.2.2': - resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.2.2': - resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.2.2': - resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.2.2': - resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.2.2': - resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.2.2': - resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 - - '@tiptap/core@3.22.3': - resolution: {integrity: sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==} - peerDependencies: - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-blockquote@3.22.3': - resolution: {integrity: sha512-IaUx3zh7yLHXzIXKL+fw/jzFhsIImdhJyw0lMhe8FfYrefFqXJFYW/sey6+L/e8B3AWvTksPA6VBwefzbH77JA==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-bold@3.22.3': - resolution: {integrity: sha512-tysipHla2zCWr8XNIWRaW9O+7i7/SoEqnRqSRUUi2ailcJjlia+RBy3RykhkgyThrQDStu5KGBS/UvrXwA+O1A==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-bubble-menu@3.22.3': - resolution: {integrity: sha512-Y6zQjh0ypDg32HWgICEvmPSKjGLr39k3aDxxt/H0uQEZSfw4smT0hxUyyyjVjx68C6t6MTnwdfz0hPI5lL68vQ==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-bullet-list@3.22.3': - resolution: {integrity: sha512-xOmW/b1hgECIE6r3IeZvKn4VVlG3+dfTjCWE6lnnyLaqdNkNhKS1CwUmDZdYNLUS2ryIUtgz5ID1W/8A3PhbiA==} - peerDependencies: - '@tiptap/extension-list': ^3.22.3 - - '@tiptap/extension-code-block-lowlight@3.22.3': - resolution: {integrity: sha512-NGFuD9zb1QfnCgD2zW4XaUEdQvd/ydm9FmXXh8eawx/+C8xt3p21DIKRERvxWrCvvVNzIUEpZRRYehPOJiD1eg==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/extension-code-block': ^3.22.3 - '@tiptap/pm': ^3.22.3 - highlight.js: ^11 - lowlight: ^2 || ^3 - - '@tiptap/extension-code-block@3.22.3': - resolution: {integrity: sha512-RiQtEjDAPrHpdo6sw6b7fOw/PijqgFIsozKKkGcSeBgWHQuFg7q9OxJTj+l0e60rVwSu/5gmKEEobzM9bX+t2Q==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-code@3.22.3': - resolution: {integrity: sha512-wafWTDQOuMKtXpZEuk1PFQmzopabBciNLryL90MB9S03MNLaQQZYLnmYkDBlzAaLAbgF5QiC+2XZQEBQuTVjFQ==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-document@3.22.3': - resolution: {integrity: sha512-MCSr1PFPtTd++lA3H1RNgqAczAE59XXJ5wUFIQf2F+/0DPY5q2SU4g5QsNJVxPPft5mrNT4C6ty8xBPrALFEdA==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-dropcursor@3.22.3': - resolution: {integrity: sha512-taXq9Tl5aybdFbptJtFRHX9LFJzbXphAbPp4/vutFyTrBu5meXDxuS+B9pEmE+Or0XcolTlW2nDZB0Tqnr18JQ==} - peerDependencies: - '@tiptap/extensions': ^3.22.3 - - '@tiptap/extension-floating-menu@3.22.3': - resolution: {integrity: sha512-0f8b4KZ3XKai8GXWseIYJGdOfQr3evtFbBo3U08zy2aYzMMXWG0zEF7qe5/oiYp2aZ95edjjITnEceviTsZkIg==} - peerDependencies: - '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-gapcursor@3.22.3': - resolution: {integrity: sha512-L/Px4UeQEVG/D9WIlcAOIej+4wyIBCMUSYicSR+hW68UsObe4rxVbUas1QgidQKm6DOhoT7U7D4KQHA/Gdg/7A==} - peerDependencies: - '@tiptap/extensions': ^3.22.3 - - '@tiptap/extension-hard-break@3.22.3': - resolution: {integrity: sha512-J0v8I99y9tbvVmgKYKzKP/JYNsWaZYS7avn4rzLft2OhnyTfwt3OoY8DtpHmmi6apSUaCtoWHWta/TmoEfK1nQ==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-heading@3.22.3': - resolution: {integrity: sha512-XBHuhiEV2EEhZHpOLcplLqAmBIhJciU3I6AtwmqeEqDC0P114uMEfAO7JGlbBZdCYotNer26PKnu44TBTeNtkw==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-history@3.22.3': - resolution: {integrity: sha512-3kev2iP84YPjwUk8BaM28EQiVHbBmE0lkcIjh/6yvgRxRwblzRwyJuFcsVOY5ouBVxl9Rg1rcFFCAs8CFNFkUg==} - peerDependencies: - '@tiptap/extensions': ^3.22.3 - - '@tiptap/extension-horizontal-rule@3.22.3': - resolution: {integrity: sha512-wI2bFzScs+KgWeBH/BtypcVKeYelCyqV0RG8nxsZMWtPrBhqixzNd0Oi3gEKtjSjKUqMQ/kjJAIRuESr5UzlHA==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-image@3.22.3': - resolution: {integrity: sha512-Qpp8c5LOQaNpHrzjqZtoxtIR+8sSqJ7k8v+8anmYw3nxjvt2kpfT28Vd7aWMX55ZS43LaxMx+MkZqbmgUmMP0w==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-italic@3.22.3': - resolution: {integrity: sha512-LteA4cb4EGCiUtrK2JHvDF/Zg0/YqV4DUyHhAAho+oGEQDupZlsS6m0ia5wQcclkiTLzsoPrwcSNu6RDGQ16wQ==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-link@3.22.3': - resolution: {integrity: sha512-S8/P2o9pv6B3kqLjH2TRWwSAximGbciNc6R8/QcN6HWLYxp0N0JoqN3rZHl9VWIBAGRWc4zkt80dhqrl2xmgfQ==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-list-item@3.22.3': - resolution: {integrity: sha512-80CNf4oO5y8+LdckT4CyMe1t01EyhpRrQC9H45JW20P7559Nrchp5my3vvMtIAJbpTPPZtcB7LwdzWGKsG5drg==} - peerDependencies: - '@tiptap/extension-list': ^3.22.3 - - '@tiptap/extension-list-keymap@3.22.3': - resolution: {integrity: sha512-pKuyj5llu35zd/s2u/H9aydKZjmPRAIK5P1q/YXULhhCNln2RnmuRfQ5NklAqTD3yGciQ2lxDwwf7J6iw3ergA==} - peerDependencies: - '@tiptap/extension-list': ^3.22.3 - - '@tiptap/extension-list@3.22.3': - resolution: {integrity: sha512-rqvv/dtqwbX+8KnPv0eMYp6PnBcuhPMol5cv1GlS8Nq/Cxt68EWGUHBuTFesw+hdnRQLmKwzoO1DlRn7PhxYRQ==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/extension-ordered-list@3.22.3': - resolution: {integrity: sha512-orAghtmd+K4Euu4BgI1hG+iZDXBYOyl5YTwiLBc2mQn+pqtZ9LqaH2us4ETwEwNP3/IWXGSAimUZ19nuL+eM2w==} - peerDependencies: - '@tiptap/extension-list': ^3.22.3 - - '@tiptap/extension-paragraph@3.22.3': - resolution: {integrity: sha512-oO7rhfyhEuwm+50s9K3GZPjYyEEEvFAvm1wXopvZnhbkBLydIWImBfrZoC5IQh4/sRDlTIjosV2C+ji5y0tUSg==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-placeholder@3.22.3': - resolution: {integrity: sha512-7vbtlDVO00odqCnsMSmA4b6wjL5PFdfExFsdsDO0K0VemqHZ/doIRx/tosNUD1VYSOyKQd8U7efUjkFyVoIPlg==} - peerDependencies: - '@tiptap/extensions': ^3.22.3 - - '@tiptap/extension-strike@3.22.3': - resolution: {integrity: sha512-jY2InoUlKkuk5KHoIDGdML1OCA2n6PRHAtxwHNkAmiYh0Khf0zaVPGFpx4dgQrN7W5Q1WE6oBZnjrvy6qb7w0g==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-text@3.22.3': - resolution: {integrity: sha512-Q9R7JsTdomP5uUjtPjNKxHT1xoh/i9OJZnmgJLe7FcgZEaPOQ3bWxmKZoLZQfDfZjyB8BtH+Hc7nUvhCMOePxw==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-typography@3.22.3': - resolution: {integrity: sha512-QQv1KUHsr3LTGhtPIG/ZP15Cj5VmI5ChgK4Q7UZ7JZBppBKAeMnyAnOcO2ith+qZ8qp5s+UiQ7Unp0zP1VSYkg==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extension-underline@3.22.3': - resolution: {integrity: sha512-Ch6CBWRa5w90yYSPUW6x9Py9JdrXMqk3pZ9OIlMYD8A7BqyZGfiHerX7XDMYDS09KjyK3U9XH60/zxYOzXdDLA==} - peerDependencies: - '@tiptap/core': ^3.22.3 - - '@tiptap/extensions@3.22.3': - resolution: {integrity: sha512-s5eiMq0m5N6N+W7dU6rd60KgZyyCD7FvtPNNswISfPr12EQwJBfbjWwTqd0UKNzA4fNrhQEERXnzORkykttPeA==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/markdown@3.22.3': - resolution: {integrity: sha512-Ajw8AkAae7IET1sxZqNv+dhZQSuLY0AHWocowTny1azyiBtmRRKygkGK0ysVA+k6UxmBD6K+tvnbpjQ0/ACl4A==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - - '@tiptap/pm@3.22.3': - resolution: {integrity: sha512-NjfWjZuvrqmpICT+GZWNIjtOdhPyqFKDMtQy7tsQ5rErM9L2ZQdy/+T/BKSO1JdTeBhdg9OP+0yfsqoYp2aT6A==} - - '@tiptap/starter-kit@3.22.3': - resolution: {integrity: sha512-vdW/Oo1fdwTL1VOQ5YYbTov00ANeHLquBVEZyL/EkV7Xv5io9rXQsCysJfTSHhiQlyr2MtWFB4+CPGuwXjQWOQ==} - - '@tiptap/suggestion@3.22.3': - resolution: {integrity: sha512-m2c+5gDj2vW7UI1J4JHCKehQUVE12qBhgF+DC+WEWUU8ZrFNf5OEYWQHDNsopa5RRpilfKfhPNbMtXgvGOsk6g==} - peerDependencies: - '@tiptap/core': ^3.22.3 - '@tiptap/pm': ^3.22.3 - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -1816,77 +894,6 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@typescript-eslint/eslint-plugin@8.58.2': - resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.58.2 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/parser@8.58.2': - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/project-service@8.58.2': - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/scope-manager@8.58.2': - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.58.2': - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/type-utils@8.58.2': - resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/types@8.58.2': - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.58.2': - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/utils@8.58.2': - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/visitor-keys@8.58.2': - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@use-gesture/core@10.3.1': - resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} - - '@use-gesture/vanilla@10.3.1': - resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==} - '@vitest/expect@4.1.0': resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} @@ -1916,19 +923,11 @@ packages: '@vitest/utils@4.1.0': resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1946,10 +945,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -1958,32 +953,13 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - bits-ui@2.17.3: - resolution: {integrity: sha512-Bef41uY9U2jaBJHPhcPvmBNkGec5Wx2z6eioDsTmsaR2vH4QoaOcPi75gzCG3+/2TNr6v/qBwzgWNPYCxNtrEA==} - engines: {node: '>=20'} - peerDependencies: - '@internationalized/date': ^3.8.1 - svelte: ^5.33.0 - blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2009,10 +985,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2027,32 +999,14 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - core-js@3.49.0: - resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} - - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - custom-event-polyfill@1.0.7: - resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2062,17 +1016,6 @@ packages: supports-color: optional: true - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2081,34 +1024,14 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.7.1: - resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dompurify@3.4.0: - resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} - - emoji-picker-element@1.29.1: - resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==} - - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} - engines: {node: '>=10.13.0'} - enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -2120,104 +1043,17 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-svelte@3.17.0: - resolution: {integrity: sha512-sF6wgd5FLS2P8CCaOy2HdYYYEcZ6TwL251dLHUkNmtLnWECk1Dwc+j6VeulmmnFxr7Xs0WNtjweOA+bJ0PnaFw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.2.0: - resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrap@2.2.5: - resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} - peerDependencies: - '@typescript-eslint/types': ^8.2.0 - peerDependenciesMeta: - '@typescript-eslint/types': - optional: true - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - event-target-polyfill@0.0.4: resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} @@ -2228,19 +1064,10 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2253,10 +1080,6 @@ packages: picomatch: optional: true - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2265,20 +1088,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2299,18 +1111,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} - engines: {node: '>=18'} - - globals@17.5.0: - resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} - engines: {node: '>=18'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -2318,13 +1118,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} - engines: {node: '>=12.0.0'} - - hls.js@1.6.16: - resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} - hono@4.12.8: resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} engines: {node: '>=16.9.0'} @@ -2341,20 +1134,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inline-style-parser@0.2.7: - resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - - is-emoji-supported@0.0.5: - resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2367,9 +1146,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -2397,32 +1173,13 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - known-css-properties@0.37.0: - resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -2493,10 +1250,6 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -2504,55 +1257,20 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loadjs@4.3.0: - resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lowlight@3.3.0: - resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - markdown-it@14.1.1: - resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} - hasBin: true - - marked@17.0.6: - resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} - engines: {node: '>= 20'} - hasBin: true - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2561,40 +1279,18 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mini-svg-data-uri@1.4.4: - resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} - hasBin: true - miniflare@4.20260205.0: resolution: {integrity: sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==} engines: {node: '>=18.0.0'} hasBin: true - miniflare@4.20260410.0: - resolution: {integrity: sha512-94LEU8d+XPVGp18eW4+bu1v7Tnq7srhqWMIsrx2jhSkdbTnGqg1I613R0GKY4eygBYl9MbqXEhzK/bczJb6uMg==} - engines: {node: '>=18.0.0'} - hasBin: true - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mode-watcher@1.1.0: - resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} - peerDependencies: - svelte: ^5.27.0 - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2611,12 +1307,6 @@ packages: engines: {node: ^18 || >=20} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - number-flow@0.6.0: - resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2624,13 +1314,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - orderedmap@2.1.1: - resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} - outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -2642,18 +1325,10 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -2746,21 +1421,6 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - plyr@3.8.4: - resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==} - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -2779,22 +1439,6 @@ packages: yaml: optional: true - postcss-safe-parser@7.0.1: - resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} - engines: {node: '>=18.0'} - peerDependencies: - postcss: ^8.4.31 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} - engines: {node: '>=4'} - postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -2815,71 +1459,6 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-plugin-svelte@3.5.1: - resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - - prettier-plugin-tailwindcss@0.7.2: - resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} - engines: {node: '>=20.19'} - peerDependencies: - '@ianvs/prettier-plugin-sort-imports': '*' - '@prettier/plugin-hermes': '*' - '@prettier/plugin-oxc': '*' - '@prettier/plugin-pug': '*' - '@shopify/prettier-plugin-liquid': '*' - '@trivago/prettier-plugin-sort-imports': '*' - '@zackad/prettier-plugin-twig': '*' - prettier: ^3.0 - prettier-plugin-astro: '*' - prettier-plugin-css-order: '*' - prettier-plugin-jsdoc: '*' - prettier-plugin-marko: '*' - prettier-plugin-multiline-arrays: '*' - prettier-plugin-organize-attributes: '*' - prettier-plugin-organize-imports: '*' - prettier-plugin-sort-imports: '*' - prettier-plugin-svelte: '*' - peerDependenciesMeta: - '@ianvs/prettier-plugin-sort-imports': - optional: true - '@prettier/plugin-hermes': - optional: true - '@prettier/plugin-oxc': - optional: true - '@prettier/plugin-pug': - optional: true - '@shopify/prettier-plugin-liquid': - optional: true - '@trivago/prettier-plugin-sort-imports': - optional: true - '@zackad/prettier-plugin-twig': - optional: true - prettier-plugin-astro: - optional: true - prettier-plugin-css-order: - optional: true - prettier-plugin-jsdoc: - optional: true - prettier-plugin-marko: - optional: true - prettier-plugin-multiline-arrays: - optional: true - prettier-plugin-organize-attributes: - optional: true - prettier-plugin-organize-imports: - optional: true - prettier-plugin-sort-imports: - optional: true - prettier-plugin-svelte: - optional: true - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -2890,81 +1469,12 @@ packages: engines: {node: '>=14'} hasBin: true - prosemirror-changeset@2.4.1: - resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} - - prosemirror-collab@1.3.1: - resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} - - prosemirror-commands@1.7.1: - resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} - - prosemirror-dropcursor@1.8.2: - resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} - - prosemirror-gapcursor@1.4.1: - resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} - - prosemirror-history@1.5.0: - resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} - - prosemirror-inputrules@1.5.1: - resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} - - prosemirror-keymap@1.2.3: - resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - - prosemirror-markdown@1.13.4: - resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} - - prosemirror-menu@1.3.0: - resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} - - prosemirror-model@1.25.4: - resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} - - prosemirror-schema-basic@1.2.4: - resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} - - prosemirror-schema-list@1.5.1: - resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} - - prosemirror-state@1.4.4: - resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} - - prosemirror-tables@1.8.5: - resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} - - prosemirror-trailing-node@3.0.0: - resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} - peerDependencies: - prosemirror-model: ^1.22.1 - prosemirror-state: ^1.4.2 - prosemirror-view: ^1.33.8 - - prosemirror-transform@1.12.0: - resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} - - prosemirror-view@1.41.8: - resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - rangetouch@2.0.1: - resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -2973,10 +1483,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - regexparam@3.0.0: - resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} - engines: {node: '>=8'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2993,50 +1499,14 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-rc.9: - resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rollup@4.60.1: resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - runed@0.23.4: - resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} - peerDependencies: - svelte: ^5.7.0 - - runed@0.25.0: - resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==} - peerDependencies: - svelte: ^5.7.0 - - runed@0.28.0: - resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} - peerDependencies: - svelte: ^5.7.0 - - runed@0.35.1: - resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} - peerDependencies: - '@sveltejs/kit': ^2.21.0 - svelte: ^5.7.0 - peerDependenciesMeta: - '@sveltejs/kit': - optional: true - - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3045,9 +1515,6 @@ packages: engines: {node: '>=10'} hasBin: true - set-cookie-parser@3.1.0: - resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3067,10 +1534,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3107,9 +1570,6 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - style-to-object@1.0.14: - resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -3119,77 +1579,6 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - svelte-check@4.4.6: - resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' - - svelte-eslint-parser@1.6.0: - resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-sonner@1.1.0: - resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==} - peerDependencies: - svelte: ^5.0.0 - - svelte-tiptap@3.0.1: - resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==} - peerDependencies: - '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.0.0 - '@tiptap/extension-bubble-menu': ^3.0.0 - '@tiptap/extension-floating-menu': ^3.0.0 - '@tiptap/pm': ^3.0.0 - svelte: ^5.0.0 - - svelte-toolbelt@0.10.6: - resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} - engines: {node: '>=18', pnpm: '>=8.7.0'} - peerDependencies: - svelte: ^5.30.2 - - svelte-toolbelt@0.7.1: - resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} - engines: {node: '>=18', pnpm: '>=8.7.0'} - peerDependencies: - svelte: ^5.0.0 - - svelte@5.55.4: - resolution: {integrity: sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==} - engines: {node: '>=18'} - - tabbable@6.4.0: - resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} - - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} - - tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} - engines: {node: '>=16.x', pnpm: '>=7.x'} - peerDependencies: - tailwind-merge: '>=3.0.0' - tailwindcss: '*' - peerDependenciesMeta: - tailwind-merge: - optional: true - - tailwindcss@4.2.2: - resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} - - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} - engines: {node: '>=6'} - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -3223,20 +1612,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -3267,34 +1646,15 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typescript-eslint@8.58.2: - resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - typescript@6.0.2: - resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} - engines: {node: '>=14.17'} - hasBin: true - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} @@ -3305,10 +1665,6 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} - engines: {node: '>=20.18.1'} - unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -3319,66 +1675,6 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - url-polyfill@1.1.14: - resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - valibot@1.3.1: - resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - - vite@8.0.0: - resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.0.0-alpha.31 - esbuild: ^0.27.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3422,14 +1718,6 @@ packages: yaml: optional: true - vitefu@1.1.3: - resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - vite: - optional: true - vitest@4.1.0: resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3465,26 +1753,6 @@ packages: jsdom: optional: true - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - - web-haptics@0.0.6: - resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==} - peerDependencies: - react: '>=18' - react-dom: '>=18' - svelte: '>=4' - vue: '>=3' - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - svelte: - optional: true - vue: - optional: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3495,24 +1763,11 @@ packages: engines: {node: '>=8'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - workerd@1.20260205.0: resolution: {integrity: sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==} engines: {node: '>=16'} hasBin: true - workerd@1.20260410.1: - resolution: {integrity: sha512-T/GRD6Y5vN9g4CnGmOlfST1w7bj+1IjRFvX0K7CodZPJuPVPNPGhz8Wppah0WdT6A7I8Kad3zgZ2OkDdWtENrg==} - engines: {node: '>=16'} - hasBin: true - - worktop@0.8.0-next.18: - resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} - engines: {node: '>=12'} - wrangler@4.63.0: resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} engines: {node: '>=20.0.0'} @@ -3523,16 +1778,6 @@ packages: '@cloudflare/workers-types': optional: true - wrangler@4.82.2: - resolution: {integrity: sha512-SKfW21sTJUkM/Qd8zc9oc8TBkAWHRsXuTxE6XdToC55Ct84pR+IfRdaTjCTuC0dL+KYvauSvSn2rtqS2Ae+Dcw==} - engines: {node: '>=20.3.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260410.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -3549,14 +1794,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - yaml@1.10.3: - resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} - engines: {node: '>= 6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -3567,27 +1804,13 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - snapshots: '@atcute/atproto@3.1.10': dependencies: '@atcute/lexicons': 1.2.9 - '@atcute/atproto@3.1.11': - dependencies: - '@atcute/lexicons': 1.3.0 - - '@atcute/bluesky-richtext-segmenter@3.0.0': {} - - '@atcute/bluesky@3.3.2': - dependencies: - '@atcute/atproto': 3.1.11 - '@atcute/lexicons': 1.3.0 - - '@atcute/car@5.1.1': + '@atcute/car@5.1.1': dependencies: '@atcute/cbor': 2.3.2 '@atcute/cid': 2.4.1 @@ -3694,37 +1917,6 @@ snapshots: dependencies: '@atcute/uint8array': 1.1.1 - '@atcute/oauth-crypto@0.1.0': - dependencies: - '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 - '@badrap/valita': 0.4.6 - nanoid: 5.1.7 - - '@atcute/oauth-keyset@0.1.0': - dependencies: - '@atcute/oauth-crypto': 0.1.0 - - '@atcute/oauth-node-client@1.1.0': - dependencies: - '@atcute/client': 4.2.1 - '@atcute/identity': 1.1.4 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicons': 1.2.9 - '@atcute/oauth-crypto': 0.1.0 - '@atcute/oauth-keyset': 0.1.0 - '@atcute/oauth-types': 0.1.1 - '@atcute/util-fetch': 1.0.5 - '@badrap/valita': 0.4.6 - nanoid: 5.1.7 - - '@atcute/oauth-types@0.1.1': - dependencies: - '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 - '@atcute/oauth-keyset': 0.1.0 - '@badrap/valita': 0.4.6 - '@atcute/repo@0.1.3': dependencies: '@atcute/car': 5.1.1 @@ -3735,12 +1927,6 @@ snapshots: '@atcute/mst': 1.0.0 '@atcute/uint8array': 1.1.1 - '@atcute/tid@1.1.2': - dependencies: - '@atcute/time-ms': 1.3.2 - - '@atcute/time-ms@1.3.2': {} - '@atcute/uint8array@1.1.1': {} '@atcute/util-fetch@1.0.5': @@ -3769,19 +1955,6 @@ snapshots: '@badrap/valita': 0.4.6 nanoid: 5.1.7 - '@atmo-dev/contrail@0.1.1(pg@8.20.0)': - dependencies: - '@atcute/atproto': 3.1.11 - '@atcute/client': 4.2.1 - '@atcute/identity': 1.1.4 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/jetstream': 1.1.2 - '@atcute/lexicons': 1.3.0 - '@atcute/xrpc-server': 0.1.12 - hono: 4.12.8 - optionalDependencies: - pg: 8.20.0 - '@babel/runtime@7.29.2': {} '@badrap/valita@0.4.6': {} @@ -3937,56 +2110,27 @@ snapshots: optionalDependencies: workerd: 1.20260205.0 - '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260410.1)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260410.1 - '@cloudflare/workerd-darwin-64@1.20260205.0': optional: true - '@cloudflare/workerd-darwin-64@1.20260410.1': - optional: true - '@cloudflare/workerd-darwin-arm64@1.20260205.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260410.1': - optional: true - '@cloudflare/workerd-linux-64@1.20260205.0': optional: true - '@cloudflare/workerd-linux-64@1.20260410.1': - optional: true - '@cloudflare/workerd-linux-arm64@1.20260205.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20260410.1': - optional: true - '@cloudflare/workerd-windows-64@1.20260205.0': optional: true - '@cloudflare/workerd-windows-64@1.20260410.1': - optional: true - '@cloudflare/workers-types@4.20260205.0': {} - '@cloudflare/workers-types@4.20260414.1': {} - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.9.0': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -4003,11 +2147,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -4016,327 +2155,81 @@ snapshots: '@esbuild/aix-ppc64@0.27.0': optional: true - '@esbuild/aix-ppc64@0.27.3': - optional: true - '@esbuild/android-arm64@0.27.0': optional: true - '@esbuild/android-arm64@0.27.3': - optional: true - '@esbuild/android-arm@0.27.0': optional: true - '@esbuild/android-arm@0.27.3': - optional: true - '@esbuild/android-x64@0.27.0': optional: true - '@esbuild/android-x64@0.27.3': - optional: true - '@esbuild/darwin-arm64@0.27.0': optional: true - '@esbuild/darwin-arm64@0.27.3': - optional: true - '@esbuild/darwin-x64@0.27.0': optional: true - '@esbuild/darwin-x64@0.27.3': - optional: true - '@esbuild/freebsd-arm64@0.27.0': optional: true - '@esbuild/freebsd-arm64@0.27.3': - optional: true - '@esbuild/freebsd-x64@0.27.0': optional: true - '@esbuild/freebsd-x64@0.27.3': - optional: true - '@esbuild/linux-arm64@0.27.0': optional: true - '@esbuild/linux-arm64@0.27.3': - optional: true - '@esbuild/linux-arm@0.27.0': optional: true - '@esbuild/linux-arm@0.27.3': - optional: true - '@esbuild/linux-ia32@0.27.0': optional: true - '@esbuild/linux-ia32@0.27.3': - optional: true - '@esbuild/linux-loong64@0.27.0': optional: true - '@esbuild/linux-loong64@0.27.3': - optional: true - '@esbuild/linux-mips64el@0.27.0': optional: true - '@esbuild/linux-mips64el@0.27.3': - optional: true - '@esbuild/linux-ppc64@0.27.0': optional: true - '@esbuild/linux-ppc64@0.27.3': - optional: true - '@esbuild/linux-riscv64@0.27.0': optional: true - '@esbuild/linux-riscv64@0.27.3': - optional: true - '@esbuild/linux-s390x@0.27.0': optional: true - '@esbuild/linux-s390x@0.27.3': - optional: true - '@esbuild/linux-x64@0.27.0': optional: true - '@esbuild/linux-x64@0.27.3': - optional: true - '@esbuild/netbsd-arm64@0.27.0': optional: true - '@esbuild/netbsd-arm64@0.27.3': - optional: true - '@esbuild/netbsd-x64@0.27.0': optional: true - '@esbuild/netbsd-x64@0.27.3': - optional: true - '@esbuild/openbsd-arm64@0.27.0': optional: true - '@esbuild/openbsd-arm64@0.27.3': - optional: true - '@esbuild/openbsd-x64@0.27.0': optional: true - '@esbuild/openbsd-x64@0.27.3': - optional: true - '@esbuild/openharmony-arm64@0.27.0': optional: true - '@esbuild/openharmony-arm64@0.27.3': - optional: true - '@esbuild/sunos-x64@0.27.0': optional: true - '@esbuild/sunos-x64@0.27.3': - optional: true - '@esbuild/win32-arm64@0.27.0': optional: true - '@esbuild/win32-arm64@0.27.3': - optional: true - '@esbuild/win32-ia32@0.27.0': optional: true - '@esbuild/win32-ia32@0.27.3': - optional: true - '@esbuild/win32-x64@0.27.0': optional: true - '@esbuild/win32-x64@0.27.3': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0(jiti@2.6.1))': - dependencies: - eslint: 10.2.0(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/compat@2.0.5(eslint@10.2.0(jiti@2.6.1))': - dependencies: - '@eslint/core': 1.2.1 - optionalDependencies: - eslint: 10.2.0(jiti@2.6.1) - - '@eslint/config-array@0.23.5': - dependencies: - '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.5.5': - dependencies: - '@eslint/core': 1.2.1 - - '@eslint/core@1.2.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/js@10.0.1(eslint@10.2.0(jiti@2.6.1))': - optionalDependencies: - eslint: 10.2.0(jiti@2.6.1) - - '@eslint/object-schema@3.0.5': {} - - '@eslint/plugin-kit@0.7.1': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/utils@0.2.11': {} - - '@fontsource-variable/geist-mono@5.2.7': {} - - '@fontsource-variable/geist@5.2.8': {} - - '@foxui/core@0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': - dependencies: - '@fontsource-variable/geist': 5.2.8 - '@fontsource-variable/geist-mono': 5.2.7 - '@jis3r/icons': 2.7.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - '@lucide/svelte': 1.8.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - clsx: 2.1.1 - dompurify: 3.4.0 - mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - tailwind-merge: 3.5.0 - tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) - tailwindcss: 4.2.2 - web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - react - - react-dom - - vue - - '@foxui/social@0.8.10(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': - dependencies: - '@atcute/bluesky': 3.3.2 - '@atcute/bluesky-richtext-segmenter': 3.0.0 - '@foxui/core': 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@foxui/text': 0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@foxui/time': 0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@use-gesture/vanilla': 10.3.1 - bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - emoji-picker-element: 1.29.1 - hls.js: 1.6.16 - is-emoji-supported: 0.0.5 - plyr: 3.8.4 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - '@tiptap/extension-code-block' - - '@tiptap/extensions' - - highlight.js - - react - - react-dom - - vue - - '@foxui/text@0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@foxui/core': 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-code-block-lowlight': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(highlight.js@11.11.1)(lowlight@3.3.0) - '@tiptap/extension-document': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-dropcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-floating-menu': 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-history': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-image': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-link': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-paragraph': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-placeholder': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-strike': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-text': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-typography': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-underline': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/markdown': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - '@tiptap/starter-kit': 3.22.3 - '@tiptap/suggestion': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - lowlight: 3.3.0 - state: link:@tiptap/pm/state - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - '@tiptap/extension-code-block' - - '@tiptap/extensions' - - highlight.js - - react - - react-dom - - vue - - '@foxui/time@0.8.5(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': - dependencies: - '@foxui/core': 0.9.1(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) - '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - react - - react-dom - - vue - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -4440,24 +2333,11 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@internationalized/date@3.12.0': - dependencies: - '@swc/helpers': 0.5.21 - - '@jis3r/icons@2.7.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))': - dependencies: - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -4472,10 +2352,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lucide/svelte@1.8.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))': - dependencies: - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -4498,13 +2374,6 @@ snapshots: '@mary-ext/simple-event-emitter@1.0.0': {} - '@napi-rs/wasm-runtime@1.1.1': - dependencies: - '@emnapi/core': 1.9.0 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 - optional: true - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -4526,26 +2395,14 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@number-flow/svelte@0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))': - dependencies: - esm-env: 1.2.2 - number-flow: 0.6.0 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - '@optique/core@0.6.11': {} '@optique/run@0.6.11': dependencies: '@optique/core': 0.6.11 - '@oxc-project/runtime@0.115.0': {} - - '@oxc-project/types@0.115.0': {} - '@oxc-project/types@0.124.0': {} - '@polka/url@1.0.0-next.29': {} - '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -4558,80 +2415,42 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@remirror/core-constants@3.0.0': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.9': - optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': dependencies: '@emnapi/core': 1.9.2 @@ -4639,27 +2458,14 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': - dependencies: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} - '@rolldown/pluginutils@1.0.0-rc.9': {} - '@rollup/rollup-android-arm-eabi@4.60.1': optional: true @@ -4741,316 +2547,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': - dependencies: - acorn: 8.16.0 - - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.82.2(@cloudflare/workers-types@4.20260414.1))': - dependencies: - '@cloudflare/workers-types': 4.20260414.1 - '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - worktop: 0.8.0-next.18 - wrangler: 4.82.2(@cloudflare/workers-types@4.20260414.1) - - '@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - '@types/cookie': 0.6.0 - acorn: 8.16.0 - cookie: 0.6.0 - devalue: 5.7.1 - esm-env: 1.2.2 - kleur: 4.1.5 - magic-string: 0.30.21 - mrmime: 2.0.1 - set-cookie-parser: 3.1.0 - sirv: 3.0.2 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - optionalDependencies: - typescript: 6.0.2 - - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - deepmerge: 4.3.1 - magic-string: 0.30.21 - obug: 2.1.1 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - vitefu: 1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - - '@swc/helpers@0.5.21': - dependencies: - tslib: 2.8.1 - - '@tailwindcss/forms@0.5.11(tailwindcss@4.2.2)': - dependencies: - mini-svg-data-uri: 1.4.4 - tailwindcss: 4.2.2 - - '@tailwindcss/node@4.2.2': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.2.2 - - '@tailwindcss/oxide-android-arm64@4.2.2': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.2.2': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.2.2': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - optional: true - - '@tailwindcss/oxide@4.2.2': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - - '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - - '@tiptap/core@3.22.3(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/pm': 3.22.3 - - '@tiptap/extension-blockquote@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-bold@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/extension-bullet-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-code-block-lowlight@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(highlight.js@11.11.1)(lowlight@3.3.0)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/extension-code-block': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - highlight.js: 11.11.1 - lowlight: 3.3.0 - - '@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/extension-code@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-document@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-dropcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/extension-gapcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-hard-break@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-heading@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-history@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-horizontal-rule@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/extension-image@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-italic@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-link@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - linkifyjs: 4.3.2 - - '@tiptap/extension-list-item@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-list-keymap@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/extension-ordered-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-paragraph@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-placeholder@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - - '@tiptap/extension-strike@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-text@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-typography@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extension-underline@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - - '@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/markdown@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - marked: 17.0.6 - - '@tiptap/pm@3.22.3': - dependencies: - prosemirror-changeset: 2.4.1 - prosemirror-collab: 1.3.1 - prosemirror-commands: 1.7.1 - prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.4.1 - prosemirror-history: 1.5.0 - prosemirror-inputrules: 1.5.1 - prosemirror-keymap: 1.2.3 - prosemirror-markdown: 1.13.4 - prosemirror-menu: 1.3.0 - prosemirror-model: 1.25.4 - prosemirror-schema-basic: 1.2.4 - prosemirror-schema-list: 1.5.1 - prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.5 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) - prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 - - '@tiptap/starter-kit@3.22.3': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/extension-blockquote': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-bold': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-bullet-list': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-code': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-code-block': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-document': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-dropcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-gapcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-hard-break': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-heading': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-horizontal-rule': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-italic': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-link': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-list-item': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-list-keymap': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-ordered-list': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) - '@tiptap/extension-paragraph': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-strike': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-text': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extension-underline': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) - '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - - '@tiptap/suggestion@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': - dependencies: - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -5061,29 +2557,10 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cookie@0.6.0': {} - '@types/deep-eql@4.0.2': {} - '@types/esrecurse@4.3.1': {} - '@types/estree@1.0.8': {} - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/json-schema@7.0.15': {} - - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@2.0.0': {} - '@types/node@12.20.55': {} '@types/node@25.5.0': @@ -5096,107 +2573,6 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 - '@types/trusted-types@2.0.7': {} - - '@types/unist@3.0.3': {} - - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 10.2.0(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 - debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 - debug: 4.4.3 - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.58.2': - dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 - - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': - dependencies: - typescript: 6.0.2 - - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.58.2': {} - - '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': - dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - eslint: 10.2.0(jiti@2.6.1) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.58.2': - dependencies: - '@typescript-eslint/types': 8.58.2 - eslint-visitor-keys: 5.0.1 - - '@use-gesture/core@10.3.1': {} - - '@use-gesture/vanilla@10.3.1': - dependencies: - '@use-gesture/core': 10.3.1 - '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 @@ -5206,13 +2582,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0))': + '@vitest/mocker@4.1.0(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0) + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0) '@vitest/pretty-format@4.1.0': dependencies: @@ -5238,19 +2614,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn@8.16.0: {} - ajv@6.14.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -5263,39 +2628,16 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.1: {} - array-union@2.1.0: {} assertion-error@2.0.1: {} - axobject-query@4.1.0: {} - - balanced-match@4.0.4: {} - better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - bits-ui@2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/dom': 1.7.6 - '@internationalized/date': 3.12.0 - esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - tabbable: 6.4.0 - transitivePeerDependencies: - - '@sveltejs/kit' - blake3-wasm@2.1.5: {} - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -5315,8 +2657,6 @@ snapshots: dependencies: readdirp: 4.1.2 - clsx@2.1.1: {} - commander@4.1.1: {} confbox@0.1.8: {} @@ -5325,66 +2665,31 @@ snapshots: convert-source-map@2.0.0: {} - cookie@0.6.0: {} - cookie@1.1.1: {} - core-js@3.49.0: {} - - crelt@1.0.6: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - cssesc@3.0.0: {} - - custom-event-polyfill@1.0.7: {} - debug@4.4.3: dependencies: ms: 2.1.3 - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - dequal@2.0.3: {} - detect-indent@6.1.0: {} detect-libc@2.1.2: {} - devalue@5.7.1: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dompurify@3.4.0: - optionalDependencies: - '@types/trusted-types': 2.0.7 - - emoji-picker-element@1.29.1: {} - - enhanced-resolve@5.20.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.2 - enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - entities@4.5.0: {} - error-stack-parser-es@1.0.5: {} es-module-lexer@2.0.0: {} @@ -5418,160 +2723,20 @@ snapshots: '@esbuild/win32-ia32': 0.27.0 '@esbuild/win32-x64': 0.27.0 - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)): - dependencies: - eslint: 10.2.0(jiti@2.6.1) - - eslint-plugin-svelte@3.17.0(eslint@10.2.0(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.2.0(jiti@2.6.1) - esutils: 2.0.3 - globals: 16.5.0 - known-css-properties: 0.37.0 - postcss: 8.5.8 - postcss-load-config: 3.1.4(postcss@8.5.8) - postcss-safe-parser: 7.0.1(postcss@8.5.8) - semver: 7.7.4 - svelte-eslint-parser: 1.6.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - optionalDependencies: - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - transitivePeerDependencies: - - ts-node - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.2.0(jiti@2.6.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.5.5 - '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.6.1 - transitivePeerDependencies: - - supports-color - esm-env@1.2.2: {} - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - esprima@4.0.1: {} - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrap@2.2.5(@typescript-eslint/types@8.58.2): - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - optionalDependencies: - '@typescript-eslint/types': 8.58.2 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 - esutils@2.0.3: {} - event-target-polyfill@0.0.4: {} expect-type@1.3.0: {} extendable-error@0.1.7: {} - fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5580,10 +2745,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5592,10 +2753,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5605,24 +2762,12 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.2 rollup: 4.60.1 - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -5646,14 +2791,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@16.5.0: {} - - globals@17.5.0: {} - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -5665,10 +2802,6 @@ snapshots: graceful-fs@4.2.11: {} - highlight.js@11.11.1: {} - - hls.js@1.6.16: {} - hono@4.12.8: {} human-id@4.1.3: {} @@ -5679,14 +2812,6 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} - - imurmurhash@0.1.4: {} - - inline-style-parser@0.2.7: {} - - is-emoji-supported@0.0.5: {} - is-extglob@2.1.1: {} is-glob@4.0.3: @@ -5695,10 +2820,6 @@ snapshots: is-number@7.0.0: {} - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -5707,7 +2828,8 @@ snapshots: isexe@2.0.0: {} - jiti@2.6.1: {} + jiti@2.6.1: + optional: true joycon@3.1.1: {} @@ -5720,29 +2842,12 @@ snapshots: dependencies: argparse: 2.0.1 - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - kleur@4.1.5: {} - known-css-properties@0.37.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - lightningcss-android-arm64@1.32.0: optional: true @@ -5792,59 +2897,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - lilconfig@2.1.0: {} - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - linkifyjs@4.3.2: {} - load-tsconfig@0.2.5: {} - loadjs@4.3.0: {} - - locate-character@3.0.0: {} - locate-path@5.0.0: dependencies: p-locate: 4.1.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - lodash.startcase@4.4.0: {} - lowlight@3.3.0: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - highlight.js: 11.11.1 - - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - markdown-it@14.1.1: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - marked@17.0.6: {} - - mdurl@2.0.0: {} - merge2@1.4.1: {} micromatch@4.0.8: @@ -5852,8 +2920,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - mini-svg-data-uri@1.4.4: {} - miniflare@4.20260205.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -5866,22 +2932,6 @@ snapshots: - bufferutil - utf-8-validate - miniflare@4.20260410.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.24.4 - workerd: 1.20260410.1 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.5 - mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -5889,16 +2939,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mode-watcher@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - runed: 0.25.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - svelte-toolbelt: 0.7.1(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - mri@1.2.0: {} - mrmime@2.0.1: {} - ms@2.1.3: {} mz@2.7.0: @@ -5911,27 +2953,10 @@ snapshots: nanoid@5.1.7: {} - natural-compare@1.4.0: {} - - number-flow@0.6.0: - dependencies: - esm-env: 1.2.2 - object-assign@4.1.1: {} obug@2.1.1: {} - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - orderedmap@2.1.1: {} - outdent@0.5.0: {} p-filter@2.1.0: @@ -5942,18 +2967,10 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - p-map@2.1.0: {} p-try@2.2.0: {} @@ -6029,41 +3046,14 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - plyr@3.8.4: - dependencies: - core-js: 3.49.0 - custom-event-polyfill: 1.0.7 - loadjs: 4.3.0 - rangetouch: 2.0.1 - url-polyfill: 1.1.14 - - postcss-load-config@3.1.4(postcss@8.5.8): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.3 - optionalDependencies: - postcss: 8.5.8 - - postcss-load-config@6.0.1(postcss@8.5.8)(tsx@4.21.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 optionalDependencies: + jiti: 2.6.1 postcss: 8.5.8 tsx: 4.21.0 - postcss-safe-parser@7.0.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-scss@4.0.9(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -6080,136 +3070,14 @@ snapshots: dependencies: xtend: 4.0.2 - prelude-ls@1.2.1: {} - - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - prettier: 3.8.1 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)))(prettier@3.8.1): - dependencies: - prettier: 3.8.1 - optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - prettier@2.8.8: {} prettier@3.8.1: {} - prosemirror-changeset@2.4.1: - dependencies: - prosemirror-transform: 1.12.0 - - prosemirror-collab@1.3.1: - dependencies: - prosemirror-state: 1.4.4 - - prosemirror-commands@1.7.1: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - - prosemirror-dropcursor@1.8.2: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 - - prosemirror-gapcursor@1.4.1: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 - - prosemirror-history@1.5.0: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 - rope-sequence: 1.3.4 - - prosemirror-inputrules@1.5.1: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - - prosemirror-keymap@1.2.3: - dependencies: - prosemirror-state: 1.4.4 - w3c-keyname: 2.2.8 - - prosemirror-markdown@1.13.4: - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.1 - prosemirror-model: 1.25.4 - - prosemirror-menu@1.3.0: - dependencies: - crelt: 1.0.6 - prosemirror-commands: 1.7.1 - prosemirror-history: 1.5.0 - prosemirror-state: 1.4.4 - - prosemirror-model@1.25.4: - dependencies: - orderedmap: 2.1.1 - - prosemirror-schema-basic@1.2.4: - dependencies: - prosemirror-model: 1.25.4 - - prosemirror-schema-list@1.5.1: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - - prosemirror-state@1.4.4: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 - - prosemirror-tables@1.8.5: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 - - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8): - dependencies: - '@remirror/core-constants': 3.0.0 - escape-string-regexp: 4.0.0 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 - - prosemirror-transform@1.12.0: - dependencies: - prosemirror-model: 1.25.4 - - prosemirror-view@1.41.8: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.12.0 - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - quansync@0.2.11: {} queue-microtask@1.2.3: {} - rangetouch@2.0.1: {} - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -6219,8 +3087,6 @@ snapshots: readdirp@4.1.2: {} - regexparam@3.0.0: {} - resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -6248,27 +3114,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - rolldown@1.0.0-rc.9: - dependencies: - '@oxc-project/types': 0.115.0 - '@rolldown/pluginutils': 1.0.0-rc.9 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-x64': 1.0.0-rc.9 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 - rollup@4.60.1: dependencies: '@types/estree': 1.0.8 @@ -6300,46 +3145,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 - rope-sequence@1.3.4: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - runed@0.23.4(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - esm-env: 1.2.2 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - runed@0.25.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - esm-env: 1.2.2 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - runed@0.28.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - esm-env: 1.2.2 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - runed@0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - dequal: 2.0.3 - esm-env: 1.2.2 - lz-string: 1.5.0 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - optionalDependencies: - '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - - sade@1.8.1: - dependencies: - mri: 1.2.0 - safer-buffer@2.1.2: {} semver@7.7.4: {} - set-cookie-parser@3.1.0: {} - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -6381,12 +3194,6 @@ snapshots: signal-exit@4.1.0: {} - sirv@3.0.2: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - slash@3.0.0: {} source-map-js@1.2.1: {} @@ -6412,10 +3219,6 @@ snapshots: strip-bom@3.0.0: {} - style-to-object@1.0.14: - dependencies: - inline-style-parser: 0.2.7 - sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -6428,95 +3231,6 @@ snapshots: supports-color@10.2.2: {} - svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.4) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - typescript: 6.0.2 - transitivePeerDependencies: - - picomatch - - svelte-eslint-parser@1.6.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - postcss: 8.5.8 - postcss-scss: 4.0.9(postcss@8.5.8) - postcss-selector-parser: 7.1.1 - semver: 7.7.4 - optionalDependencies: - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - svelte-sonner@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - runed: 0.28.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) - '@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/extension-floating-menu': 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) - '@tiptap/pm': 3.22.3 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - svelte-toolbelt@0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - style-to-object: 1.0.14 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - transitivePeerDependencies: - - '@sveltejs/kit' - - svelte-toolbelt@0.7.1(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - dependencies: - clsx: 2.1.1 - runed: 0.23.4(svelte@5.55.4(@typescript-eslint/types@8.58.2)) - style-to-object: 1.0.14 - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - - svelte@5.55.4(@typescript-eslint/types@8.58.2): - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 - '@types/trusted-types': 2.0.7 - acorn: 8.16.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.7.1 - esm-env: 1.2.2 - esrap: 2.2.5(@typescript-eslint/types@8.58.2) - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - transitivePeerDependencies: - - '@typescript-eslint/types' - - tabbable@6.4.0: {} - - tailwind-merge@3.5.0: {} - - tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2): - dependencies: - tailwindcss: 4.2.2 - optionalDependencies: - tailwind-merge: 3.5.0 - - tailwindcss@4.2.2: {} - - tapable@2.3.2: {} - term-size@2.2.1: {} thenify-all@1.6.0: @@ -6544,19 +3258,14 @@ snapshots: dependencies: is-number: 7.0.0 - totalist@3.0.1: {} - tree-kill@1.2.2: {} - ts-api-utils@2.5.0(typescript@6.0.2): - dependencies: - typescript: 6.0.2 - ts-interface-checker@0.1.13: {} - tslib@2.8.1: {} + tslib@2.8.1: + optional: true - tsup@8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.0) cac: 6.7.14 @@ -6567,7 +3276,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.8)(tsx@4.21.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0) resolve-from: 5.0.0 rollup: 4.60.1 source-map: 0.7.6 @@ -6591,37 +3300,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-fest@4.41.0: {} - typescript-eslint@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): - dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - eslint: 10.2.0(jiti@2.6.1) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - typescript@5.9.3: {} - typescript@6.0.2: {} - - uc.micro@2.1.0: {} - ufo@1.6.3: {} undici-types@7.18.2: {} undici@7.18.2: {} - undici@7.24.4: {} - unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -6630,33 +3318,7 @@ snapshots: universalify@0.1.2: {} - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - url-polyfill@1.1.14: {} - - util-deprecate@1.0.2: {} - - valibot@1.3.1(typescript@6.0.2): - optionalDependencies: - typescript: 6.0.2 - - vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0): - dependencies: - '@oxc-project/runtime': 0.115.0 - lightningcss: 1.32.0 - picomatch: 4.0.3 - postcss: 8.5.8 - rolldown: 1.0.0-rc.9 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.0 - fsevents: 2.3.3 - tsx: 4.21.0 - - vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): + vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -6665,19 +3327,15 @@ snapshots: tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.5.0 - esbuild: 0.27.3 + esbuild: 0.27.0 fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 - vitefu@1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): - optionalDependencies: - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - - vitest@4.1.0(@types/node@25.5.0)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0)): + vitest@4.1.0(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0)) + '@vitest/mocker': 4.1.0(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -6694,19 +3352,13 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0) + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.5.0 transitivePeerDependencies: - msw - w3c-keyname@2.2.8: {} - - web-haptics@0.0.6(svelte@5.55.4(@typescript-eslint/types@8.58.2)): - optionalDependencies: - svelte: 5.55.4(@typescript-eslint/types@8.58.2) - which@2.0.2: dependencies: isexe: 2.0.0 @@ -6716,8 +3368,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.5: {} - workerd@1.20260205.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260205.0 @@ -6726,19 +3376,6 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260205.0 '@cloudflare/workerd-windows-64': 1.20260205.0 - workerd@1.20260410.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260410.1 - '@cloudflare/workerd-darwin-arm64': 1.20260410.1 - '@cloudflare/workerd-linux-64': 1.20260410.1 - '@cloudflare/workerd-linux-arm64': 1.20260410.1 - '@cloudflare/workerd-windows-64': 1.20260410.1 - - worktop@0.8.0-next.18: - dependencies: - mrmime: 2.0.1 - regexparam: 3.0.0 - wrangler@4.63.0(@cloudflare/workers-types@4.20260205.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -6756,31 +3393,10 @@ snapshots: - bufferutil - utf-8-validate - wrangler@4.82.2(@cloudflare/workers-types@4.20260414.1): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260410.1) - blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260410.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260410.1 - optionalDependencies: - '@cloudflare/workers-types': 4.20260414.1 - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - ws@8.18.0: {} xtend@4.0.2: {} - yaml@1.10.3: {} - - yocto-queue@0.1.0: {} - yocto-queue@1.2.2: {} youch-core@0.3.3: @@ -6795,5 +3411,3 @@ snapshots: '@speed-highlight/core': 1.2.14 cookie: 1.1.1 youch-core: 0.3.3 - - zimmerframe@1.1.4: {} diff --git a/realtime-lexicon-templates/subscribe.json b/realtime-lexicon-templates/subscribe.json new file mode 100644 index 0000000..fe0f8a7 --- /dev/null +++ b/realtime-lexicon-templates/subscribe.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "tools.atmo.realtime.subscribe", + "defs": { + "main": { + "type": "query", + "description": "Open a streaming subscription. Responds with Server-Sent Events by default, or upgrades to WebSocket when the request carries `Upgrade: websocket`. Auth via `?ticket=` (browser) or `Authorization: Bearer ` (server-side). The stream closes with a final `member.removed` event if the caller loses access mid-stream.", + "parameters": { + "type": "params", + "properties": { + "ticket": { "type": "string", "description": "Subscription ticket from `.ticket`. Required if no Authorization header." }, + "topic": { "type": "string", "description": "Optional: restrict the stream to a single topic (must be in the ticket). Required on the JWT path." }, + "collection": { "type": "string", "format": "nsid", "description": "Optional: drop `record.*` events whose collection doesn't match." } + } + }, + "errors": [ + { "name": "AuthRequired" }, + { "name": "Forbidden" }, + { "name": "InvalidRequest" }, + { "name": "NotFound" } + ] + } + } +} diff --git a/realtime-lexicon-templates/ticket.json b/realtime-lexicon-templates/ticket.json new file mode 100644 index 0000000..3fa9010 --- /dev/null +++ b/realtime-lexicon-templates/ticket.json @@ -0,0 +1,40 @@ +{ + "lexicon": 1, + "id": "tools.atmo.realtime.ticket", + "defs": { + "main": { + "type": "procedure", + "description": "Mint a short-lived subscription ticket for the given topic. The caller's access is checked and — for `community:` topics — expanded to the concrete list of `space:` topics the caller can see. Server-side callers may skip tickets and use their JWT directly on `.subscribe`.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["topic"], + "properties": { + "topic": { + "type": "string", + "description": "Requested topic. One of `space:`, `community:`, `actor:` (self-only in v1)." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ticket", "topics", "expiresAt"], + "properties": { + "ticket": { "type": "string", "description": "HMAC-signed opaque token. Passed as `?ticket=` on `.subscribe`." }, + "topics": { "type": "array", "items": { "type": "string" }, "description": "Concrete delivery topics authorized by this ticket." }, + "expiresAt": { "type": "integer", "description": "Unix ms." } + } + } + }, + "errors": [ + { "name": "Forbidden" }, + { "name": "NotFound" }, + { "name": "NotSupported" } + ] + } + } +} diff --git a/src/core/realtime/durable-object.ts b/src/core/realtime/durable-object.ts new file mode 100644 index 0000000..6dc6964 --- /dev/null +++ b/src/core/realtime/durable-object.ts @@ -0,0 +1,294 @@ +/** Durable Object backend for realtime PubSub. + * + * Two pieces live here: + * 1. `RealtimePubSubDO` — the DO class. Ship it from your Worker via + * `export { RealtimePubSubDO } from "@atmo-dev/contrail";` and bind it in + * your `wrangler.toml`. One DO = one topic; addressed by name. + * 2. `DurableObjectPubSub` — client-side adapter implementing the PubSub + * interface against a DO namespace binding. + * + * Wire format between Worker and DO (internal, not a stable public contract): + * POST /publish — body = RealtimeEvent JSON + * GET /subscribe — server-sent events stream, optionally with + * `Upgrade: websocket` for WS connections. + * Auth/ACL is already checked at the Worker edge; + * the DO trusts anything that reaches it. */ + +import type { PubSub, RealtimeEvent } from "./types"; + +// ---- Minimal structural typings so we don't depend on @cloudflare/workers-types +// at the library level. Callers on Workers will have proper types. +// ---------------------------------------------------------------------------- + +export interface DurableObjectId { + toString(): string; +} + +export interface DurableObjectStub { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; +} + +export interface DurableObjectNamespace { + idFromName(name: string): DurableObjectId; + get(id: DurableObjectId): DurableObjectStub; +} + +export interface DurableObjectState { + acceptWebSocket(ws: any, tags?: string[]): void; + getWebSockets(tag?: string): any[]; +} + +// ---------------------------------------------------------------------------- +// Client adapter +// ---------------------------------------------------------------------------- + +export class DurableObjectPubSub implements PubSub { + constructor(private readonly namespace: DurableObjectNamespace) {} + + private stub(topic: string): DurableObjectStub { + return this.namespace.get(this.namespace.idFromName(topic)); + } + + async publish(event: RealtimeEvent): Promise { + const res = await this.stub(event.topic).fetch("https://do/publish", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(event), + }); + if (!res.ok) { + // Consume the body so the edge doesn't hold a dangling response. + await res.text().catch(() => ""); + throw new Error(`DO publish failed (${res.status})`); + } + } + + /** In-Worker server-side subscribe. Browsers should hit the SSE endpoint + * directly; this is the path for an in-process consumer that wants an + * AsyncIterable (tests, bots embedded in the Worker). */ + subscribe(topic: string, signal?: AbortSignal): AsyncIterable { + const stub = this.stub(topic); + return { + [Symbol.asyncIterator]() { + return pullIterator(stub, signal); + }, + }; + } +} + +function pullIterator( + stub: DurableObjectStub, + signal?: AbortSignal +): AsyncIterator { + let reader: ReadableStreamDefaultReader | null = null; + let buf = ""; + const decoder = new TextDecoder(); + const ac = new AbortController(); + if (signal) { + if (signal.aborted) ac.abort(); + else signal.addEventListener("abort", () => ac.abort(), { once: true }); + } + + const open = async () => { + const res = await stub.fetch("https://do/subscribe", { + method: "GET", + headers: { accept: "text/event-stream" }, + signal: ac.signal, + }); + if (!res.ok || !res.body) throw new Error(`DO subscribe failed (${res.status})`); + reader = res.body.getReader(); + }; + + return { + async next(): Promise> { + if (!reader) await open(); + while (true) { + // Drain buffered frames. + while (true) { + const sep = buf.indexOf("\n\n"); + if (sep < 0) break; + const frame = buf.slice(0, sep); + buf = buf.slice(sep + 2); + let data: string | null = null; + for (const line of frame.split("\n")) { + if (line.startsWith(":")) continue; + if (line.startsWith("data:")) data = line.slice(5).trim(); + } + if (data) { + try { + return { value: JSON.parse(data) as RealtimeEvent, done: false }; + } catch { + /* skip malformed */ + } + } + } + if (ac.signal.aborted) return { value: undefined, done: true }; + const r = await reader!.read(); + if (r.done) return { value: undefined, done: true }; + buf += decoder.decode(r.value, { stream: true }); + } + }, + async return(): Promise> { + ac.abort(); + try { + await reader?.cancel(); + } catch { + /* ignore */ + } + return { value: undefined, done: true }; + }, + }; +} + +// ---------------------------------------------------------------------------- +// Durable Object class +// ---------------------------------------------------------------------------- + +/** The Durable Object implementation. Each DO instance owns the fan-out for + * exactly one topic. WebSocket connections are stored via the Hibernation + * API (`state.acceptWebSocket`) so idle rooms cost near-zero. + * + * This class intentionally avoids the `DurableObject` base class so we don't + * have to depend on @cloudflare/workers-types at the library level — users + * wire it up directly in their Worker entry. */ +export class RealtimePubSubDO { + constructor( + protected readonly state: DurableObjectState, + _env?: unknown + ) {} + + /** Worker entry delegates `fetch` to this method. */ + async fetch(request: Request): Promise { + const url = new URL(request.url); + if (request.method === "POST" && url.pathname === "/publish") { + let event: RealtimeEvent; + try { + event = (await request.json()) as RealtimeEvent; + } catch { + return new Response(JSON.stringify({ error: "InvalidRequest" }), { status: 400 }); + } + this.publishEvent(event); + return new Response("{}", { status: 200 }); + } + if (request.method === "GET" && url.pathname === "/subscribe") { + const did = url.searchParams.get("did") ?? undefined; + if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") { + const Pair = (globalThis as unknown as { WebSocketPair?: any }).WebSocketPair; + if (!Pair) return new Response("websockets require Workers", { status: 426 }); + const pair = new Pair(); + this.acceptWebSocketSubscriber(pair[1], did); + return new Response(null, { + status: 101, + // @ts-expect-error Workers-specific init field + webSocket: pair[0], + }); + } + return this.openSseResponse(did); + } + return new Response("not found", { status: 404 }); + } + + /** Fan-out an event to every connected subscriber (WS + SSE). + * Public so tests + advanced callers can skip the HTTP layer. */ + publishEvent(event: RealtimeEvent): void { + const payload = JSON.stringify(event); + const frame = `event: ${event.kind}\ndata: ${payload}\n\n`; + + for (const ws of this.state.getWebSockets()) { + try { + ws.send(payload); + } catch { + /* ignore — socket may be closing */ + } + const attachment = getAttachment(ws); + if ( + event.kind === "member.removed" && + attachment?.did && + event.payload.did === attachment.did + ) { + try { + ws.close(4003, "membership-revoked"); + } catch { + /* ignore */ + } + } + } + for (const entry of this.sseControllers) { + try { + entry.controller.enqueue(this.encoder.encode(frame)); + } catch { + /* drop; cleanup happens on the subscribe-side */ + } + if ( + event.kind === "member.removed" && + entry.did && + event.payload.did === entry.did + ) { + try { + entry.controller.close(); + } catch { + /* ignore */ + } + } + } + } + + /** Register a server-side WebSocket as a subscriber. Wires the DID attachment. */ + acceptWebSocketSubscriber(serverWs: any, did?: string): void { + this.state.acceptWebSocket(serverWs, did ? [did] : undefined); + if (did) setAttachment(serverWs, { did }); + } + + /** Open an SSE subscriber; returns the streaming Response. */ + openSseResponse(did?: string): Response { + let entry: SseEntry; + const stream = new ReadableStream({ + start: (controller) => { + entry = { controller, did }; + this.sseControllers.add(entry); + controller.enqueue(this.encoder.encode(`: open\n\n`)); + }, + cancel: () => { + this.sseControllers.delete(entry); + }, + }); + return new Response(stream, { + status: 200, + headers: { + "content-type": "text/event-stream", + "cache-control": "no-cache, no-transform", + connection: "keep-alive", + }, + }); + } + + private readonly sseControllers = new Set(); + private readonly encoder = new TextEncoder(); +} + +interface SseEntry { + controller: ReadableStreamDefaultController; + did: string | undefined; +} + +interface WsAttachment { + did?: string; +} + +function setAttachment(ws: any, attachment: WsAttachment): void { + try { + ws.serializeAttachment?.(attachment); + } catch { + /* non-hibernating socket — fall back to a direct property */ + ws.__attachment = attachment; + } +} + +function getAttachment(ws: any): WsAttachment | null { + try { + const a = ws.deserializeAttachment?.(); + if (a) return a as WsAttachment; + } catch { + /* ignore */ + } + return (ws.__attachment as WsAttachment | undefined) ?? null; +} diff --git a/src/core/realtime/in-memory.ts b/src/core/realtime/in-memory.ts new file mode 100644 index 0000000..311c0a5 --- /dev/null +++ b/src/core/realtime/in-memory.ts @@ -0,0 +1,116 @@ +import type { PubSub, RealtimeEvent } from "./types"; +import { DEFAULT_QUEUE_BOUND } from "./types"; + +/** Single-process PubSub backed by in-memory subscriber sets. + * + * Each subscriber owns a bounded queue; when full, oldest events are dropped. + * `publish` returns once every subscriber has been offered the event — it + * never awaits a subscriber's consumption, so a slow consumer can't block + * producers. The cost of that guarantee is the drop-oldest policy. */ +export class InMemoryPubSub implements PubSub { + private readonly subscribers = new Map>(); + private readonly queueBound: number; + + constructor(opts: { queueBound?: number } = {}) { + this.queueBound = opts.queueBound ?? DEFAULT_QUEUE_BOUND; + } + + async publish(event: RealtimeEvent): Promise { + const set = this.subscribers.get(event.topic); + if (!set) return; + for (const sub of set) sub.push(event); + } + + subscribe(topic: string, signal?: AbortSignal): AsyncIterable { + const sub = new Subscriber(this.queueBound); + let set = this.subscribers.get(topic); + if (!set) { + set = new Set(); + this.subscribers.set(topic, set); + } + set.add(sub); + + const cleanup = () => { + sub.close(); + const s = this.subscribers.get(topic); + if (s) { + s.delete(sub); + if (s.size === 0) this.subscribers.delete(topic); + } + }; + + if (signal) { + if (signal.aborted) cleanup(); + else signal.addEventListener("abort", cleanup, { once: true }); + } + + return sub.iterate(cleanup); + } + + /** Test-only: current subscriber count for a topic. */ + subscriberCount(topic: string): number { + return this.subscribers.get(topic)?.size ?? 0; + } +} + +class Subscriber { + private readonly queue: RealtimeEvent[] = []; + private pending: ((v: RealtimeEvent | null) => void) | null = null; + private closed = false; + /** Number of events dropped because the queue was full. The consumer can + * observe a gap by comparing monotonic event timestamps; exposing the + * count on a side channel is future work. */ + public droppedCount = 0; + + constructor(private readonly bound: number) {} + + push(event: RealtimeEvent): void { + if (this.closed) return; + if (this.pending) { + const p = this.pending; + this.pending = null; + p(event); + return; + } + if (this.queue.length >= this.bound) { + this.queue.shift(); + this.droppedCount += 1; + } + this.queue.push(event); + } + + close(): void { + if (this.closed) return; + this.closed = true; + if (this.pending) { + const p = this.pending; + this.pending = null; + p(null); + } + } + + iterate(cleanup: () => void): AsyncIterable { + const self = this; + return { + [Symbol.asyncIterator]() { + return { + async next(): Promise> { + if (self.queue.length > 0) { + return { value: self.queue.shift()!, done: false }; + } + if (self.closed) return { value: undefined, done: true }; + const event = await new Promise((resolve) => { + self.pending = resolve; + }); + if (event === null) return { value: undefined, done: true }; + return { value: event, done: false }; + }, + async return(): Promise> { + cleanup(); + return { value: undefined, done: true }; + }, + }; + }, + }; + } +} diff --git a/src/core/realtime/index.ts b/src/core/realtime/index.ts new file mode 100644 index 0000000..05e39ec --- /dev/null +++ b/src/core/realtime/index.ts @@ -0,0 +1,37 @@ +export { registerRealtimeRoutes } from "./router"; +export type { RealtimeRoutesOptions } from "./router"; +export { InMemoryPubSub } from "./in-memory"; +export { DurableObjectPubSub, RealtimePubSubDO } from "./durable-object"; +export type { + DurableObjectId, + DurableObjectNamespace, + DurableObjectStub, + DurableObjectState, +} from "./durable-object"; +export { TicketSigner } from "./ticket"; +export type { TicketPayload } from "./ticket"; +export { wrapWithPublishing } from "./publishing-adapter"; +export { sseResponse } from "./sse"; +export { pumpWebSocket } from "./websocket"; +export type { WebSocketLike } from "./websocket"; +export { mergeAsyncIterables } from "./merge"; +export { resolveTopicForCaller } from "./resolve"; +export type { TopicResolution, TopicResolutionContext, TopicResolutionError } from "./resolve"; +export type { + PubSub, + RealtimeConfig, + RealtimeEvent, + RealtimeEventKind, +} from "./types"; +export { + actorTopic, + collectionTopic, + communityTopic, + parseCommunityTopic, + parseSpaceTopic, + spaceTopic, + isCommunityTopic, + DEFAULT_KEEPALIVE_MS, + DEFAULT_QUEUE_BOUND, + DEFAULT_TICKET_TTL_MS, +} from "./types"; diff --git a/src/core/realtime/merge.ts b/src/core/realtime/merge.ts new file mode 100644 index 0000000..e0fa7df --- /dev/null +++ b/src/core/realtime/merge.ts @@ -0,0 +1,77 @@ +/** Merge N AsyncIterables into one, interleaving events as they arrive. + * Terminates when every source terminates, or when `signal` aborts. */ + +export function mergeAsyncIterables( + sources: AsyncIterable[], + signal?: AbortSignal +): AsyncIterable { + if (sources.length === 0) { + return { + async *[Symbol.asyncIterator]() { + /* nothing to yield */ + }, + }; + } + + return { + [Symbol.asyncIterator]() { + const iterators = sources.map((s) => s[Symbol.asyncIterator]()); + // One in-flight next() per source, racing each other. + type Slot = { + idx: number; + promise: Promise<{ idx: number; result: IteratorResult }>; + }; + const pending = new Map(); + let doneCount = 0; + + const schedule = (idx: number) => { + const slot: Slot = { + idx, + promise: iterators[idx]! + .next() + .then((result) => ({ idx, result })), + }; + pending.set(idx, slot); + }; + + for (let i = 0; i < iterators.length; i++) schedule(i); + + const cleanup = () => { + for (const it of iterators) { + try { + it.return?.(); + } catch { + /* ignore */ + } + } + }; + + if (signal) { + if (signal.aborted) cleanup(); + else signal.addEventListener("abort", cleanup, { once: true }); + } + + return { + async next(): Promise> { + while (pending.size > 0) { + const slots = [...pending.values()]; + const { idx, result } = await Promise.race(slots.map((s) => s.promise)); + pending.delete(idx); + if (result.done) { + doneCount += 1; + if (doneCount === iterators.length) return { value: undefined, done: true }; + continue; + } + schedule(idx); + return { value: result.value, done: false }; + } + return { value: undefined, done: true }; + }, + async return(): Promise> { + cleanup(); + return { value: undefined, done: true }; + }, + }; + }, + }; +} diff --git a/src/core/realtime/publishing-adapter.ts b/src/core/realtime/publishing-adapter.ts new file mode 100644 index 0000000..edffe0a --- /dev/null +++ b/src/core/realtime/publishing-adapter.ts @@ -0,0 +1,139 @@ +/** Decorator that wraps a spaces StorageAdapter and publishes realtime events + * after successful writes. Spaces and community modules stay unaware of + * realtime; the decorator is the only integration seam. */ + +import type { StorageAdapter, SpaceMemberRow } from "../spaces/types"; +import type { PubSub, RealtimeEvent } from "./types"; +import { communityTopic, spaceTopic } from "./types"; + +export interface PublishingAdapterOptions { + /** Optional lookup: given a space's ownerDid, return true if that DID is a + * community in the local `communities` table. When provided, writes also + * publish to `community:` so subscribers who expanded that alias + * at ticket-mint time receive the event. + * + * The lookup is expected to be cheap (cached in the caller) — the decorator + * calls it on every write. */ + isCommunityDid?: (did: string) => Promise | boolean; +} + +export function wrapWithPublishing( + inner: StorageAdapter, + pubsub: PubSub, + opts: PublishingAdapterOptions = {} +): StorageAdapter { + const publishSpaceAndCommunity = async ( + spaceUri: string, + ownerDid: string | null, + build: (topic: string) => RealtimeEvent + ): Promise => { + await pubsub.publish(build(spaceTopic(spaceUri))); + if (ownerDid && opts.isCommunityDid && (await opts.isCommunityDid(ownerDid))) { + await pubsub.publish(build(communityTopic(ownerDid))); + } + }; + + const ownerOf = async (spaceUri: string): Promise => { + const s = await inner.getSpace(spaceUri); + return s?.ownerDid ?? null; + }; + + const wrapped: StorageAdapter = { + ...inner, + createSpace: inner.createSpace.bind(inner), + getSpace: inner.getSpace.bind(inner), + listSpaces: inner.listSpaces.bind(inner), + deleteSpace: inner.deleteSpace.bind(inner), + updateSpaceAppPolicy: inner.updateSpaceAppPolicy.bind(inner), + getMember: inner.getMember.bind(inner), + listMembers: inner.listMembers.bind(inner), + createInvite: inner.createInvite.bind(inner), + listInvites: inner.listInvites.bind(inner), + revokeInvite: inner.revokeInvite.bind(inner), + getInvite: inner.getInvite.bind(inner), + redeemInvite: inner.redeemInvite.bind(inner), + getRecord: inner.getRecord.bind(inner), + listRecords: inner.listRecords.bind(inner), + listCollections: inner.listCollections.bind(inner), + + async addMember(spaceUri, did, addedBy) { + await inner.addMember(spaceUri, did, addedBy); + const owner = await ownerOf(spaceUri); + const now = Date.now(); + await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ + topic, + kind: "member.added", + payload: { spaceUri, did }, + ts: now, + })); + }, + + async removeMember(spaceUri, did) { + await inner.removeMember(spaceUri, did); + const owner = await ownerOf(spaceUri); + const now = Date.now(); + await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ + topic, + kind: "member.removed", + payload: { spaceUri, did }, + ts: now, + })); + }, + + async applyMembershipDiff(spaceUri, adds, removes, addedBy) { + await inner.applyMembershipDiff(spaceUri, adds, removes, addedBy); + if (adds.length === 0 && removes.length === 0) return; + const owner = await ownerOf(spaceUri); + const now = Date.now(); + for (const did of adds) { + await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ + topic, + kind: "member.added", + payload: { spaceUri, did }, + ts: now, + })); + } + for (const did of removes) { + await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ + topic, + kind: "member.removed", + payload: { spaceUri, did }, + ts: now, + })); + } + }, + + async putRecord(record) { + await inner.putRecord(record); + const owner = await ownerOf(record.spaceUri); + const now = Date.now(); + await publishSpaceAndCommunity(record.spaceUri, owner, (topic) => ({ + topic, + kind: "record.created", + payload: { + spaceUri: record.spaceUri, + collection: record.collection, + authorDid: record.authorDid, + rkey: record.rkey, + }, + ts: now, + })); + }, + + async deleteRecord(spaceUri, collection, authorDid, rkey) { + await inner.deleteRecord(spaceUri, collection, authorDid, rkey); + const owner = await ownerOf(spaceUri); + const now = Date.now(); + await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ + topic, + kind: "record.deleted", + payload: { spaceUri, collection, authorDid, rkey }, + ts: now, + })); + }, + }; + return wrapped; +} + +// Keep this import hint for types that downstream code might pull from here. +export type { SpaceMemberRow }; diff --git a/src/core/realtime/resolve.ts b/src/core/realtime/resolve.ts new file mode 100644 index 0000000..1499faa --- /dev/null +++ b/src/core/realtime/resolve.ts @@ -0,0 +1,90 @@ +/** Resolve a raw topic request (as given by a caller) to the concrete set of + * delivery topics they are authorized to subscribe to. + * + * Rules (v1): + * - `space:` → allowed iff caller is owner or a member of the space. + * - `community:` → expanded to `space:` for every space in the + * community reachable by the caller (direct grants + * or via delegation). `resolveReachableSpaces` + * already has exactly this semantics. + * - `actor:` → self-only in v1. + * - `collection:` → rejected unless the deployment opts in + * (not yet implemented). */ + +import type { StorageAdapter } from "../spaces/types"; +import type { CommunityAdapter } from "../community/adapter"; +import { resolveReachableSpaces } from "../community/acl"; +import { spaceTopic, parseCommunityTopic, parseSpaceTopic } from "./types"; + +export interface TopicResolutionContext { + spaces: StorageAdapter; + /** May be null if the community module is not enabled. */ + community: CommunityAdapter | null; +} + +export interface TopicResolution { + ok: true; + topics: string[]; +} + +export interface TopicResolutionError { + ok: false; + error: "Forbidden" | "InvalidRequest" | "NotFound" | "NotSupported"; + reason: string; +} + +export async function resolveTopicForCaller( + rawTopic: string, + callerDid: string, + ctx: TopicResolutionContext +): Promise { + // space: + const spaceUri = parseSpaceTopic(rawTopic); + if (spaceUri) { + const space = await ctx.spaces.getSpace(spaceUri); + if (!space) return { ok: false, error: "NotFound", reason: "space-not-found" }; + if (space.ownerDid === callerDid) return { ok: true, topics: [rawTopic] }; + const member = await ctx.spaces.getMember(spaceUri, callerDid); + if (!member) return { ok: false, error: "Forbidden", reason: "not-member" }; + return { ok: true, topics: [rawTopic] }; + } + + // community: + const communityDid = parseCommunityTopic(rawTopic); + if (communityDid) { + if (!ctx.community) { + return { ok: false, error: "NotSupported", reason: "community-module-disabled" }; + } + const row = await ctx.community.getCommunity(communityDid); + if (!row) return { ok: false, error: "NotFound", reason: "community-not-found" }; + const reachable = await resolveReachableSpaces(ctx.community, callerDid); + // Filter to spaces owned by THIS community — reachable may include spaces + // from other communities via cross-community delegation. + const ownedList = await ctx.spaces.listSpaces({ ownerDid: communityDid, limit: 1000 }); + const owned: Set = new Set(ownedList.spaces.map((s) => s.uri)); + const topics: string[] = []; + for (const uri of reachable) { + if (owned.has(uri)) topics.push(spaceTopic(uri)); + } + if (topics.length === 0) { + return { ok: false, error: "Forbidden", reason: "no-reachable-spaces-in-community" }; + } + return { ok: true, topics }; + } + + // actor: — self-only in v1. + if (rawTopic.startsWith("actor:")) { + const did = rawTopic.slice("actor:".length); + if (did !== callerDid) { + return { ok: false, error: "Forbidden", reason: "actor-self-only" }; + } + return { ok: true, topics: [rawTopic] }; + } + + // collection: — public firehose, not exposed in v1. + if (rawTopic.startsWith("collection:")) { + return { ok: false, error: "NotSupported", reason: "collection-firehose-disabled" }; + } + + return { ok: false, error: "InvalidRequest", reason: "unknown-topic" }; +} diff --git a/src/core/realtime/router.ts b/src/core/realtime/router.ts new file mode 100644 index 0000000..a6448ef --- /dev/null +++ b/src/core/realtime/router.ts @@ -0,0 +1,221 @@ +/** Realtime XRPC routes: ticket mint + subscribe (SSE | WS). */ + +import type { Context, Hono, MiddlewareHandler } from "hono"; +import type { ContrailConfig } from "../types"; +import type { ServiceAuth } from "../spaces/auth"; +import type { StorageAdapter } from "../spaces/types"; +import type { CommunityAdapter } from "../community/adapter"; +import { InMemoryPubSub } from "./in-memory"; +import { TicketSigner } from "./ticket"; +import { sseResponse } from "./sse"; +import { pumpWebSocket, type WebSocketLike } from "./websocket"; +import { mergeAsyncIterables } from "./merge"; +import { resolveTopicForCaller } from "./resolve"; +import type { PubSub, RealtimeEvent } from "./types"; +import { DEFAULT_TICKET_TTL_MS, DEFAULT_KEEPALIVE_MS } from "./types"; + +export interface RealtimeRoutesOptions { + /** Required: auth middleware for `.realtime.ticket` (JWT required). + * The subscribe endpoint uses the *ticket* path primarily, so it does not + * require this middleware — but bots can subscribe with Authorization + * header, in which case this middleware is used as a fallback. */ + authMiddleware: MiddlewareHandler; + pubsub?: PubSub; +} + +/** WebSocketPair exists on Cloudflare Workers; on Node/Bun it's absent. + * When absent, a platform-provided WebSocket accept hook is used instead. */ +interface WebSocketPairCtor { + new (): { 0: WebSocketLike & { accept?: () => void }; 1: WebSocketLike & { accept?: () => void } }; +} + +export function registerRealtimeRoutes( + app: Hono, + config: ContrailConfig, + spaces: StorageAdapter, + community: CommunityAdapter | null, + options: RealtimeRoutesOptions +): void { + const cfg = config.realtime; + if (!cfg) return; + + const pubsub: PubSub = options.pubsub ?? cfg.pubsub ?? new InMemoryPubSub({ queueBound: cfg.queueBound }); + const signer = new TicketSigner(cfg.ticketSecret); + const ticketTtl = cfg.ticketTtlMs ?? DEFAULT_TICKET_TTL_MS; + const keepaliveMs = cfg.keepaliveMs ?? DEFAULT_KEEPALIVE_MS; + + const NS = `${config.namespace}.realtime`; + + // POST /.realtime.ticket — { topic } → { ticket, topics, expiresAt } + app.post(`/xrpc/${NS}.ticket`, options.authMiddleware, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as { topic?: string } | null; + if (!body?.topic) { + return c.json({ error: "InvalidRequest", message: "topic required" }, 400); + } + const resolved = await resolveTopicForCaller(body.topic, sa.issuer, { spaces, community }); + if (!resolved.ok) { + const status = resolved.error === "NotFound" ? 404 : resolved.error === "Forbidden" ? 403 : 400; + return c.json({ error: resolved.error, reason: resolved.reason }, status); + } + const ticket = await signer.sign({ + topics: resolved.topics, + did: sa.issuer, + ttlMs: ticketTtl, + }); + return c.json({ + ticket, + topics: resolved.topics, + expiresAt: Date.now() + ticketTtl, + }); + }); + + // GET /.realtime.subscribe — SSE or WS, driven by ?ticket= or JWT. + app.get(`/xrpc/${NS}.subscribe`, async (c) => { + const url = new URL(c.req.url); + const ticketParam = url.searchParams.get("ticket"); + const collectionFilter = url.searchParams.get("collection"); + + let callerDid: string; + let topics: string[]; + + if (ticketParam) { + const payload = await signer.verify(ticketParam); + if (!payload) { + return c.json({ error: "AuthRequired", reason: "invalid-or-expired-ticket" }, 401); + } + callerDid = payload.did; + topics = payload.topics; + // Optional: narrow to topics the query explicitly requests. + const requested = url.searchParams.get("topic"); + if (requested) { + if (!payload.topics.includes(requested)) { + return c.json({ error: "Forbidden", reason: "topic-not-in-ticket" }, 403); + } + topics = [requested]; + } + } else { + // JWT path for server-side bots. Requires the auth middleware to have + // run and attached `serviceAuth` to the context — so we invoke it + // inline. + const runAuth = options.authMiddleware; + let authed = false; + await runAuth(c, async () => { + authed = true; + }); + if (!authed) return c.res; // middleware already responded with 401 + const sa = getAuth(c); + callerDid = sa.issuer; + const topic = url.searchParams.get("topic"); + if (!topic) { + return c.json({ error: "InvalidRequest", message: "topic required" }, 400); + } + const resolved = await resolveTopicForCaller(topic, callerDid, { spaces, community }); + if (!resolved.ok) { + const status = resolved.error === "NotFound" ? 404 : resolved.error === "Forbidden" ? 403 : 400; + return c.json({ error: resolved.error, reason: resolved.reason }, status); + } + topics = resolved.topics; + } + + if (topics.length === 0) { + return c.json({ error: "InvalidRequest", reason: "no-topics" }, 400); + } + + // Build the merged iterable, with an inline filter that closes the stream + // on a matching `member.removed` event (self-kick on revocation). + const ac = new AbortController(); + const signals: AbortSignal[] = [ac.signal]; + const reqSignal = c.req.raw.signal; + if (reqSignal) signals.push(reqSignal); + const combined = anySignal(signals); + + const sources = topics.map((t) => pubsub.subscribe(t, combined)); + const merged = withSelfKickAndFilter( + mergeAsyncIterables(sources, combined), + callerDid, + collectionFilter, + ac + ); + + // Content negotiation: Upgrade: websocket → WS, else SSE. + if (c.req.header("Upgrade")?.toLowerCase() === "websocket") { + const Pair = (globalThis as unknown as { WebSocketPair?: WebSocketPairCtor }) + .WebSocketPair; + if (!Pair) { + return c.json( + { error: "NotSupported", reason: "websockets-require-worker-or-ws-adapter" }, + 426 + ); + } + const pair = new Pair(); + const clientWs = pair[0]; + const serverWs = pair[1]; + serverWs.accept?.(); + // Pump in the background; don't await. + void pumpWebSocket(serverWs, merged, combined, { keepaliveMs }); + return new Response(null, { + status: 101, + // Hono/undici-compat: some runtimes honor `webSocket` on the init. + // @ts-expect-error - Workers-specific init field + webSocket: clientWs, + }); + } + + return sseResponse(merged, combined, { keepaliveMs }); + }); +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function getAuth(c: Context): ServiceAuth { + const a = c.get("serviceAuth") as ServiceAuth | undefined; + if (!a) throw new Error("service auth not set"); + return a; +} + +/** Merge multiple AbortSignals into one. Aborts when any source aborts. */ +function anySignal(signals: AbortSignal[]): AbortSignal { + const ac = new AbortController(); + for (const s of signals) { + if (s.aborted) { + ac.abort(); + return ac.signal; + } + s.addEventListener("abort", () => ac.abort(), { once: true }); + } + return ac.signal; +} + +/** Wrap an iterable: drop events that don't pass the collection filter (if + * any), and close the outer controller as soon as we see a `member.removed` + * for the caller's own DID. */ +function withSelfKickAndFilter( + source: AsyncIterable, + callerDid: string, + collectionFilter: string | null, + ac: AbortController +): AsyncIterable { + return { + async *[Symbol.asyncIterator]() { + for await (const event of source) { + if (event.kind === "member.removed" && event.payload.did === callerDid) { + // Deliver the kick event so the client sees why, then close. + yield event; + ac.abort(); + return; + } + if ( + collectionFilter && + (event.kind === "record.created" || event.kind === "record.deleted") && + event.payload.collection !== collectionFilter + ) { + continue; + } + yield event; + } + }, + }; +} diff --git a/src/core/realtime/sse.ts b/src/core/realtime/sse.ts new file mode 100644 index 0000000..65d89c1 --- /dev/null +++ b/src/core/realtime/sse.ts @@ -0,0 +1,98 @@ +/** Server-Sent Events transport. + * + * Wraps an AsyncIterable as a streaming Response. The caller + * (the router) has already done auth and has an AbortSignal it can use to + * tear the stream down (e.g. on `member.removed` for the subscriber's DID). */ + +import type { RealtimeEvent } from "./types"; +import { DEFAULT_KEEPALIVE_MS } from "./types"; + +export interface SseOptions { + keepaliveMs?: number; + /** Called before the stream closes. Useful for cleanup that the caller + * can't do via the signal (e.g. removing a subscriber from a set). */ + onClose?: () => void; +} + +export function sseResponse( + iter: AsyncIterable, + signal: AbortSignal, + opts: SseOptions = {} +): Response { + const keepaliveMs = opts.keepaliveMs ?? DEFAULT_KEEPALIVE_MS; + const encoder = new TextEncoder(); + + const stream = new ReadableStream({ + start(controller) { + let closed = false; + let keepalive: ReturnType | null = null; + + const close = () => { + if (closed) return; + closed = true; + if (keepalive) clearInterval(keepalive); + try { + controller.close(); + } catch { + /* already closed */ + } + opts.onClose?.(); + }; + + signal.addEventListener("abort", close, { once: true }); + + keepalive = setInterval(() => { + if (closed) return; + try { + controller.enqueue(encoder.encode(`: keepalive\n\n`)); + } catch { + close(); + } + }, keepaliveMs); + + (async () => { + // Opening comment — helps some clients / proxies initialize promptly. + controller.enqueue(encoder.encode(`: open\n\n`)); + try { + for await (const event of iter) { + if (closed) break; + controller.enqueue(encoder.encode(frameEvent(event))); + } + } catch (err) { + if (!closed) { + try { + controller.enqueue( + encoder.encode( + `event: error\ndata: ${JSON.stringify({ + message: err instanceof Error ? err.message : String(err), + })}\n\n` + ) + ); + } catch { + /* stream already torn down */ + } + } + } finally { + close(); + } + })(); + }, + cancel() { + opts.onClose?.(); + }, + }); + + return new Response(stream, { + status: 200, + headers: { + "content-type": "text/event-stream", + "cache-control": "no-cache, no-transform", + connection: "keep-alive", + "x-accel-buffering": "no", + }, + }); +} + +function frameEvent(event: RealtimeEvent): string { + return `event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`; +} diff --git a/src/core/realtime/ticket.ts b/src/core/realtime/ticket.ts new file mode 100644 index 0000000..72d0595 --- /dev/null +++ b/src/core/realtime/ticket.ts @@ -0,0 +1,159 @@ +/** Subscription tickets — HMAC-signed short-lived `{topics, did, exp}` blobs. + * + * Wire format: `.` where + * payload = base64url(JSON({ topics, did, exp, iat })) + * sig = base64url(HMAC-SHA256(key, payload)) + * + * Tickets are integrity-only (not encrypted). Browsers use them because + * EventSource / WebSocket can't send Authorization headers; server-side + * consumers skip the ticket dance and send their JWT directly. */ + +export interface TicketPayload { + /** Concrete delivery topics this ticket authorizes. `community:` is + * expanded to the caller's visible spaces before signing — a ticket never + * carries a community alias. */ + topics: string[]; + did: string; + /** Unix ms. */ + exp: number; + /** Unix ms — useful for debugging; ignored on verify. */ + iat: number; +} + +function normalizeSecret(secret: Uint8Array | string): Uint8Array { + if (typeof secret !== "string") { + if (secret.length !== 32) { + throw new Error(`realtime ticketSecret must be 32 bytes, got ${secret.length}`); + } + return secret; + } + // 64 hex chars would also round-trip as base64 (to 48 bytes). Prefer hex + // when the input matches the hex alphabet exactly; fall back to base64. + const hex = tryHex(secret); + if (hex && hex.length === 32) return hex; + const b64 = tryBase64(secret); + if (b64 && b64.length === 32) return b64; + if (hex || b64) { + const got = (hex ?? b64)!.length; + throw new Error(`realtime ticketSecret must decode to 32 bytes, got ${got}`); + } + throw new Error("realtime ticketSecret must be a 32-byte Uint8Array or base64/hex string"); +} + +function tryBase64(s: string): Uint8Array | null { + try { + const normal = s.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normal + "=".repeat((4 - (normal.length % 4)) % 4); + if (!/^[A-Za-z0-9+/]*=*$/.test(padded)) return null; + const bin = atob(padded); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; + } catch { + return null; + } +} + +function tryHex(s: string): Uint8Array | null { + if (!/^[0-9a-fA-F]+$/.test(s) || s.length % 2 !== 0) return null; + const out = new Uint8Array(s.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16); + } + return out; +} + +function b64urlFromBytes(bytes: Uint8Array): string { + let bin = ""; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!); + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function b64urlToBytes(s: string): Uint8Array { + const normal = s.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normal + "=".repeat((4 - (normal.length % 4)) % 4); + const bin = atob(padded); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +function b64urlFromString(s: string): string { + return b64urlFromBytes(new TextEncoder().encode(s)); +} + +function stringFromB64url(s: string): string { + return new TextDecoder().decode(b64urlToBytes(s)); +} + +function constantTimeEq(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!; + return diff === 0; +} + +export class TicketSigner { + private readonly keyPromise: Promise; + + constructor(secret: Uint8Array | string) { + const raw = normalizeSecret(secret); + this.keyPromise = crypto.subtle.importKey( + "raw", + raw as BufferSource, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"] + ); + } + + async sign(input: { topics: string[]; did: string; ttlMs: number }): Promise { + const now = Date.now(); + const payload: TicketPayload = { + topics: input.topics, + did: input.did, + exp: now + input.ttlMs, + iat: now, + }; + const payloadPart = b64urlFromString(JSON.stringify(payload)); + const sig = await crypto.subtle.sign( + "HMAC", + await this.keyPromise, + new TextEncoder().encode(payloadPart) as BufferSource + ); + const sigPart = b64urlFromBytes(new Uint8Array(sig)); + return `${payloadPart}.${sigPart}`; + } + + /** Returns the decoded payload if the ticket is valid + unexpired, else null. */ + async verify(ticket: string): Promise { + const dot = ticket.indexOf("."); + if (dot < 0) return null; + const payloadPart = ticket.slice(0, dot); + const sigPart = ticket.slice(dot + 1); + let expectedSig: Uint8Array; + try { + expectedSig = b64urlToBytes(sigPart); + } catch { + return null; + } + const computedRaw = await crypto.subtle.sign( + "HMAC", + await this.keyPromise, + new TextEncoder().encode(payloadPart) as BufferSource + ); + const computed = new Uint8Array(computedRaw); + if (!constantTimeEq(expectedSig, computed)) return null; + let parsed: TicketPayload; + try { + parsed = JSON.parse(stringFromB64url(payloadPart)); + } catch { + return null; + } + if (!parsed || !Array.isArray(parsed.topics) || typeof parsed.did !== "string") { + return null; + } + if (typeof parsed.exp !== "number" || parsed.exp <= Date.now()) return null; + return parsed; + } +} diff --git a/src/core/realtime/types.ts b/src/core/realtime/types.ts new file mode 100644 index 0000000..c4aaff1 --- /dev/null +++ b/src/core/realtime/types.ts @@ -0,0 +1,95 @@ +/** Realtime module — canonical types + interfaces. See docs/realtime.md. */ + +/** Discriminated union of every event kind that flows through the PubSub. */ +export type RealtimeEvent = + | { + topic: string; + kind: "record.created"; + payload: { spaceUri: string; collection: string; authorDid: string; rkey: string }; + ts: number; + } + | { + topic: string; + kind: "record.deleted"; + payload: { spaceUri: string; collection: string; authorDid: string; rkey: string }; + ts: number; + } + | { + topic: string; + kind: "member.added"; + payload: { spaceUri: string; did: string }; + ts: number; + } + | { + topic: string; + kind: "member.removed"; + payload: { spaceUri: string; did: string }; + ts: number; + }; + +export type RealtimeEventKind = RealtimeEvent["kind"]; + +/** Core pubsub abstraction. Implementations: InMemoryPubSub, DurableObjectPubSub. */ +export interface PubSub { + publish(event: RealtimeEvent): Promise; + /** Stream events on the topic until the signal aborts (or the iterator is + * returned/broken out of). Implementations use a bounded per-subscriber + * queue with drop-oldest semantics — a slow subscriber can't stall publishers. */ + subscribe(topic: string, signal?: AbortSignal): AsyncIterable; +} + +// ---- Canonical topic strings ----------------------------------------------- +// `community:` is an alias resolved at ticket-mint time to the concrete +// set of `space:` topics the caller can see; it is never a real delivery +// topic. The other three are real. + +export function spaceTopic(uri: string): string { + return `space:${uri}`; +} + +export function communityTopic(did: string): string { + return `community:${did}`; +} + +export function collectionTopic(nsid: string): string { + return `collection:${nsid}`; +} + +export function actorTopic(did: string): string { + return `actor:${did}`; +} + +export function isCommunityTopic(topic: string): boolean { + return topic.startsWith("community:"); +} + +export function parseCommunityTopic(topic: string): string | null { + return isCommunityTopic(topic) ? topic.slice("community:".length) : null; +} + +export function parseSpaceTopic(topic: string): string | null { + return topic.startsWith("space:") ? topic.slice("space:".length) : null; +} + +// ---- Config ----------------------------------------------------------------- + +export interface RealtimeConfig { + /** Backing pubsub. Default: new InMemoryPubSub() (single-process only). On + * Workers, pass `new DurableObjectPubSub(env.REALTIME)`. */ + pubsub?: PubSub; + /** HMAC secret used to sign subscription tickets. 32 bytes. Accepts raw + * Uint8Array or base64 / hex string. Envelope-encrypts nothing — tickets + * are integrity-only, not confidential. */ + ticketSecret: Uint8Array | string; + /** Ticket lifetime in ms. Default 120_000 (2 minutes). */ + ticketTtlMs?: number; + /** SSE/WS keepalive interval in ms. Default 15_000. */ + keepaliveMs?: number; + /** Per-subscriber queue bound. Default 1024. Events beyond this are dropped + * oldest-first and the subscriber receives a `lag` signal (out of band). */ + queueBound?: number; +} + +export const DEFAULT_TICKET_TTL_MS = 120_000; +export const DEFAULT_KEEPALIVE_MS = 15_000; +export const DEFAULT_QUEUE_BOUND = 1024; diff --git a/src/core/realtime/websocket.ts b/src/core/realtime/websocket.ts new file mode 100644 index 0000000..833483d --- /dev/null +++ b/src/core/realtime/websocket.ts @@ -0,0 +1,84 @@ +/** WebSocket transport. + * + * Accepts a new WebSocket connection (either via `WebSocketPair` on Workers + * or a platform-provided server-side socket) and pumps events to it from an + * AsyncIterable. Messages are UTF-8 JSON, one event per frame. + * + * Close codes (subset, RFC 6455 + app-custom): + * - 4001: server error pumping + * - 4003: membership revoked + * - 4008: ticket/auth invalid (used by the router, not here) + */ + +import type { RealtimeEvent } from "./types"; +import { DEFAULT_KEEPALIVE_MS } from "./types"; + +export interface WebSocketLike { + send(data: string): void; + close(code?: number, reason?: string): void; + addEventListener(type: "message" | "close" | "error", listener: (ev: any) => void): void; +} + +export interface WebSocketPumpOptions { + keepaliveMs?: number; + onClose?: () => void; +} + +/** Pump events from `iter` to `ws` until the signal aborts or the iter ends. + * Caller is responsible for having already accept()ed the socket. */ +export async function pumpWebSocket( + ws: WebSocketLike, + iter: AsyncIterable, + signal: AbortSignal, + opts: WebSocketPumpOptions = {} +): Promise { + const keepaliveMs = opts.keepaliveMs ?? DEFAULT_KEEPALIVE_MS; + let closed = false; + + const close = (code?: number, reason?: string) => { + if (closed) return; + closed = true; + try { + ws.close(code, reason); + } catch { + /* already closed */ + } + opts.onClose?.(); + }; + + ws.addEventListener("close", () => { + closed = true; + opts.onClose?.(); + }); + ws.addEventListener("error", () => { + closed = true; + opts.onClose?.(); + }); + signal.addEventListener("abort", () => close(1000, "aborted"), { once: true }); + + const keepalive = setInterval(() => { + if (closed) return; + try { + ws.send(JSON.stringify({ kind: "$keepalive" })); + } catch { + close(); + } + }, keepaliveMs); + + try { + for await (const event of iter) { + if (closed) break; + try { + ws.send(JSON.stringify(event)); + } catch { + close(4001, "send-failed"); + break; + } + } + } catch { + close(4001, "pump-error"); + } finally { + clearInterval(keepalive); + close(); + } +} diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 7ab98fa..7baaa32 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -14,6 +14,12 @@ import type { StorageAdapter } from "../spaces/types"; import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; import { registerCommunityRoutes } from "../community/router"; import type { CommunityRoutesOptions } from "../community/router"; +import { CommunityAdapter } from "../community/adapter"; +import { registerRealtimeRoutes } from "../realtime/router"; +import type { RealtimeRoutesOptions } from "../realtime/router"; +import { InMemoryPubSub } from "../realtime/in-memory"; +import { wrapWithPublishing } from "../realtime/publishing-adapter"; +import type { PubSub } from "../realtime/types"; import { resolveActor } from "../identity"; import { resolveProfiles } from "./profiles"; import { backfillUser } from "../backfill"; @@ -26,6 +32,7 @@ export interface SpacesContext { export interface CreateAppOptions { spaces?: SpacesRoutesOptions; community?: CommunityRoutesOptions; + realtime?: Partial; /** Separate DB for the spaces tables. Defaults to `db`. */ spacesDb?: Database; /** Full spaces context override (escape hatch for tests). */ @@ -72,7 +79,7 @@ export function createApp( // Shared spaces context — verifier + adapter — reused by both the per-collection // routes (for `?spaceUri=...` dispatch) and the `.space.*` routes. const spacesDb = options.spacesDb ?? db; - const spacesCtx: SpacesContext | null = + let spacesCtx: SpacesContext | null = options.spacesCtx !== undefined ? options.spacesCtx : config.spaces @@ -82,6 +89,24 @@ export function createApp( } : null; + // Realtime pubsub is built up-front so the publishing decorator can reference + // it. The subscribe endpoint uses the same instance. + let realtimePubsub: PubSub | null = null; + if (config.realtime && spacesCtx) { + realtimePubsub = + options.realtime?.pubsub ?? config.realtime.pubsub ?? new InMemoryPubSub({ + queueBound: config.realtime.queueBound, + }); + const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; + const isCommunityDid = communityAdapter + ? cachedIsCommunityDid(communityAdapter) + : undefined; + spacesCtx = { + ...spacesCtx, + adapter: wrapWithPublishing(spacesCtx.adapter, realtimePubsub, { isCommunityDid }), + }; + } + registerAdminRoutes(app, db, config); registerCollectionRoutes(app, db, config, spacesCtx); registerFeedRoutes(app, db, config); @@ -103,5 +128,33 @@ export function createApp( ); } + if (config.realtime && spacesCtx && realtimePubsub) { + const authMiddleware = + options.realtime?.authMiddleware ?? + options.spaces?.authMiddleware ?? + createServiceAuthMiddleware(spacesCtx.verifier); + const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; + registerRealtimeRoutes(app, config, spacesCtx.adapter, communityAdapter, { + authMiddleware, + pubsub: realtimePubsub, + }); + } + return app; } + +function cachedIsCommunityDid( + community: CommunityAdapter +): (did: string) => Promise { + const TTL = 60_000; + const cache = new Map(); + return async (did: string) => { + const now = Date.now(); + const hit = cache.get(did); + if (hit && hit.expires > now) return hit.value; + const row = await community.getCommunity(did); + const value = row != null; + cache.set(did, { value, expires: now + TTL }); + return value; + }; +} diff --git a/src/core/types.ts b/src/core/types.ts index bbdeef2..688b89c 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -158,6 +158,9 @@ export interface ContrailConfig { /** Community module configuration. When set, the service exposes community XRPCs * for managing community-owned spaces and tiered access levels. Requires `spaces`. */ community?: import("./community/types").CommunityConfig; + /** Realtime module configuration. When set, the service exposes ticket + SSE/WS + * subscribe XRPCs, and wraps the spaces adapter to publish events after writes. */ + realtime?: import("./realtime/types").RealtimeConfig; /** Customize the auto-generated `.permissionSet` lexicon. */ permissionSet?: PermissionSetConfig; } diff --git a/src/generate.ts b/src/generate.ts index 6ecf411..dfc838a 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -51,6 +51,17 @@ function findCommunityTemplatesDir(rootDir: string): string | null { return null; } +function findRealtimeTemplatesDir(rootDir: string): string | null { + const candidates = [ + join(rootDir, "realtime-lexicon-templates"), + join(rootDir, "node_modules/@atmo-dev/contrail/realtime-lexicon-templates"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} + /** Yield all JSON files under a directory (recursive). */ function* walkJson(dir: string): Generator { for (const entry of readdirSync(dir, { withFileTypes: true })) { @@ -774,6 +785,48 @@ export function generateLexicons(options: GenerateOptions): Record.realtime.* --- + + if (config.realtime) { + log("Generating realtime endpoints..."); + const templatesDir = findRealtimeTemplatesDir(rootDir); + if (!templatesDir) { + log(" (realtime templates not found — skipping)"); + } else { + const templateIdRe = /^tools\.atmo\.realtime(\.[A-Za-z0-9.]+)?$/; + const idReplace = (id: string) => + id.startsWith("tools.atmo.realtime") + ? id.replace(/^tools\.atmo\.realtime/, `${ns}.realtime`) + : id; + + const rewriteRefs = (obj: any): any => { + if (Array.isArray(obj)) return obj.map(rewriteRefs); + if (obj && typeof obj === "object") { + const out: any = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === "ref" && typeof v === "string" && v.startsWith("tools.atmo.realtime")) { + out[k] = v.replace(/^tools\.atmo\.realtime/, `${ns}.realtime`); + } else if (k === "id" && typeof v === "string" && templateIdRe.test(v)) { + out[k] = idReplace(v); + } else { + out[k] = rewriteRefs(v); + } + } + return out; + } + return obj; + }; + + for (const file of walkJson(templatesDir)) { + const doc = JSON.parse(readFileSync(file, "utf-8")); + if (typeof doc.id !== "string" || !templateIdRe.test(doc.id)) continue; + const newId = idReplace(doc.id); + const rewritten = rewriteRefs({ ...doc, id: newId }); + writeLexicon(newId, rewritten); + } + } + } + // --- Permission set --- // Permission-set lexicons (https://atproto.com/guides/permission-sets) can // only reference NSIDs under the same namespace as the set itself, which diff --git a/src/index.ts b/src/index.ts index 1311579..e2cc2e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,3 +50,26 @@ export type { export { HostedAdapter } from "./core/spaces/adapter"; export { nextTid } from "./core/spaces/tid"; export { generateInviteToken, hashInviteToken } from "./core/spaces/invite-token"; + +// Realtime +export type { + PubSub, + RealtimeConfig, + RealtimeEvent, + RealtimeEventKind, +} from "./core/realtime/types"; +export { + InMemoryPubSub, + DurableObjectPubSub, + RealtimePubSubDO, + TicketSigner, + wrapWithPublishing, + sseResponse, + pumpWebSocket, + mergeAsyncIterables, + resolveTopicForCaller, + actorTopic, + collectionTopic, + communityTopic, + spaceTopic, +} from "./core/realtime"; diff --git a/tests/realtime-durable-object.test.ts b/tests/realtime-durable-object.test.ts new file mode 100644 index 0000000..237d184 --- /dev/null +++ b/tests/realtime-durable-object.test.ts @@ -0,0 +1,238 @@ +/** Unit tests for the DO class and the DO client adapter. + * + * We don't run wrangler here — instead we hand-roll a tiny fake `DurableObjectState` + * and fake WebSocket pair so we can drive the class's public `fetch` method + * directly. That's enough to verify routing, fan-out, kick-on-remove, and the + * client-side SSE pull iterator. */ + +import { describe, it, expect } from "vitest"; +import { + DurableObjectPubSub, + RealtimePubSubDO, + type DurableObjectState, + type DurableObjectNamespace, + type DurableObjectStub, +} from "../src/core/realtime/durable-object"; +import type { RealtimeEvent } from "../src/core/realtime/types"; + +// ---- Fakes ----------------------------------------------------------------- + +class FakeWebSocket { + public readonly sent: string[] = []; + public closedCode: number | undefined; + private attachment: unknown = null; + serializeAttachment(a: unknown): void { + this.attachment = a; + } + deserializeAttachment(): unknown { + return this.attachment; + } + send(s: string): void { + this.sent.push(s); + } + close(code?: number): void { + this.closedCode = code; + } + // For the test harness — required so the class can upgrade an incoming request. + accept(): void {} +} + +class FakeState implements DurableObjectState { + private readonly sockets: FakeWebSocket[] = []; + acceptWebSocket(ws: FakeWebSocket): void { + this.sockets.push(ws); + } + getWebSockets(): FakeWebSocket[] { + return this.sockets; + } +} + +function mkEvent(overrides: Partial = {}): RealtimeEvent { + return { + topic: "space:at://x/y/z", + kind: "record.created", + payload: { spaceUri: "at://x/y/z", collection: "c", authorDid: "did:plc:x", rkey: "r" }, + ts: 1, + ...(overrides as any), + }; +} + +// ---- DO class tests -------------------------------------------------------- + +describe("RealtimePubSubDO — publish/fanout", () => { + it("fans out publishEvent to all accepted websockets", async () => { + const state = new FakeState(); + const doInstance = new RealtimePubSubDO(state); + + for (const did of ["did:plc:a", "did:plc:b"]) { + doInstance.acceptWebSocketSubscriber(new FakeWebSocket(), did); + } + + doInstance.publishEvent(mkEvent({ ts: 42 })); + + const sockets = state.getWebSockets(); + expect(sockets).toHaveLength(2); + for (const ws of sockets) { + expect(ws.sent).toHaveLength(1); + expect(JSON.parse(ws.sent[0]!).ts).toBe(42); + } + }); + + it("kicks WS whose attachment.did matches a member.removed event", async () => { + const state = new FakeState(); + const doInstance = new RealtimePubSubDO(state); + + doInstance.acceptWebSocketSubscriber(new FakeWebSocket(), "did:plc:kickme"); + doInstance.acceptWebSocketSubscriber(new FakeWebSocket(), "did:plc:stay"); + + doInstance.publishEvent({ + topic: "space:x", + kind: "member.removed", + payload: { spaceUri: "at://x/y/z", did: "did:plc:kickme" }, + ts: 1, + }); + + const [kicked, staying] = state.getWebSockets(); + expect(kicked!.closedCode).toBe(4003); + expect(staying!.closedCode).toBeUndefined(); + }); + + it("delivers SSE to pull-style subscribers and closes the stream on self-kick", async () => { + const state = new FakeState(); + const doInstance = new RealtimePubSubDO(state); + + const subRes = doInstance.openSseResponse("did:plc:self"); + expect(subRes.status).toBe(200); + const reader = subRes.body!.getReader(); + + const collect = async (target: number) => { + const decoder = new TextDecoder(); + const events: RealtimeEvent[] = []; + let buf = ""; + while (events.length < target) { + const { value, done } = await reader.read(); + if (value) buf += decoder.decode(value, { stream: true }); + if (done) break; + while (true) { + const sep = buf.indexOf("\n\n"); + if (sep < 0) break; + const frame = buf.slice(0, sep); + buf = buf.slice(sep + 2); + for (const line of frame.split("\n")) { + if (line.startsWith("data:")) { + try { events.push(JSON.parse(line.slice(5).trim())); } catch {} + } + } + } + } + return events; + }; + + // Publish a regular event, then a self-targeting member.removed. + doInstance.publishEvent(mkEvent({ ts: 7 })); + doInstance.publishEvent({ + topic: "space:x", + kind: "member.removed", + payload: { spaceUri: "at://x/y/z", did: "did:plc:self" }, + ts: 8, + }); + + const events = await collect(2); + expect(events.map((e) => e.kind)).toEqual(["record.created", "member.removed"]); + + // Stream should be closed after the self-kick. + const tail = await reader.read(); + expect(tail.done).toBe(true); + }); +}); + +// ---- Client adapter tests -------------------------------------------------- + +describe("DurableObjectPubSub — client adapter", () => { + it("routes publish to the correct DO by topic name", async () => { + const calls: Array<{ name: string; url: string; init?: RequestInit }> = []; + const stubFor = (name: string): DurableObjectStub => ({ + async fetch(input, init) { + const url = typeof input === "string" ? input : (input as URL | Request).toString(); + calls.push({ name, url, init }); + return new Response("{}", { status: 200 }); + }, + }); + const ns: DurableObjectNamespace = { + idFromName: (n: string) => ({ toString: () => n }), + get: (id) => stubFor(id.toString()), + }; + const ps = new DurableObjectPubSub(ns); + + await ps.publish(mkEvent({ topic: "space:a" })); + await ps.publish(mkEvent({ topic: "community:c1" })); + + expect(calls).toHaveLength(2); + expect(calls[0]!.name).toBe("space:a"); + expect(calls[0]!.url).toContain("/publish"); + expect(calls[1]!.name).toBe("community:c1"); + }); + + it("subscribes via SSE and pulls parsed events", async () => { + // Simulate a DO that streams two SSE events, then ends. + const stream = new ReadableStream({ + start(controller) { + const enc = new TextEncoder(); + controller.enqueue(enc.encode(": open\n\n")); + controller.enqueue( + enc.encode(`event: record.created\ndata: ${JSON.stringify(mkEvent({ ts: 1 }))}\n\n`) + ); + controller.enqueue( + enc.encode(`event: record.created\ndata: ${JSON.stringify(mkEvent({ ts: 2 }))}\n\n`) + ); + controller.close(); + }, + }); + const stubFor = (): DurableObjectStub => ({ + async fetch() { + return new Response(stream, { status: 200 }); + }, + }); + const ns: DurableObjectNamespace = { + idFromName: (n) => ({ toString: () => n }), + get: () => stubFor(), + }; + const ps = new DurableObjectPubSub(ns); + + const received: RealtimeEvent[] = []; + for await (const e of ps.subscribe("space:a")) { + received.push(e); + } + expect(received.map((e) => e.ts)).toEqual([1, 2]); + }); + + it("ends the pull iterator when return() is called", async () => { + let canceled = false; + const stream = new ReadableStream({ + start() { + /* never emits */ + }, + cancel() { + canceled = true; + }, + }); + const ns: DurableObjectNamespace = { + idFromName: (n) => ({ toString: () => n }), + get: () => ({ + async fetch() { + return new Response(stream, { status: 200 }); + }, + }), + }; + const ps = new DurableObjectPubSub(ns); + const iter = ps.subscribe("space:a")[Symbol.asyncIterator](); + // Kick off open() so reader exists, then explicitly return. + const next = iter.next(); + // Let the iterator attach its reader before returning. + await new Promise((r) => setTimeout(r, 0)); + await iter.return!(); + const r = await next; + expect(r.done).toBe(true); + expect(canceled).toBe(true); + }); +}); diff --git a/tests/realtime-e2e.test.ts b/tests/realtime-e2e.test.ts new file mode 100644 index 0000000..431e1fa --- /dev/null +++ b/tests/realtime-e2e.test.ts @@ -0,0 +1,305 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; +import type { RealtimeEvent } from "../src/core/realtime/types"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; +const CHARLIE = "did:plc:charlie"; + +const MASTER_KEY = new Uint8Array(32).fill(5); +const REALTIME_SECRET = new Uint8Array(32).fill(9); + +const CONFIG: ContrailConfig = { + namespace: "test.rt", + collections: { message: { collection: "app.event.message" } }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:test.example#svc", + }, + community: { + masterKey: MASTER_KEY, + plcDirectory: "https://plc.test", + fetch: mockFetch, + resolver: mockResolver(), + }, + realtime: { + ticketSecret: REALTIME_SECRET, + keepaliveMs: 60_000, + }, +}; + +function mockResolver(): any { + return { + resolve: async (_did: string) => ({ + id: _did, + service: [ + { id: "#atproto_pds", type: "AtprotoPersonalDataServer", serviceEndpoint: "https://pds.test" }, + ], + }), + }; +} + +async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith("/xrpc/com.atproto.server.createSession")) { + return new Response( + JSON.stringify({ accessJwt: "a.b.c", refreshJwt: "r.r.r", did: "did:plc:community" }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + if (url.startsWith("https://plc.test/")) return new Response("{}", { status: 200 }); + return new Response("not found", { status: 404 }); +} + +function fakeAuth(): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { issuer: did, audience: CONFIG.spaces!.serviceDid, lxm: undefined }); + await next(); + }; +} + +async function makeApp(): Promise { + const db = createSqliteDatabase(":memory:"); + const resolved = resolveConfig(CONFIG); + await initSchema(db, resolved); + return createApp(db, resolved, { + spaces: { authMiddleware: fakeAuth() }, + }); +} + +function call( + app: Hono, + method: string, + path: string, + did: string | null, + body?: any, + extraHeaders: Record = {} +): Promise { + const headers: Record = { ...extraHeaders }; + if (did) headers["X-Test-Did"] = did; + if (body !== undefined) headers["Content-Type"] = "application/json"; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + ); +} + +/** Open an SSE stream and return an async iterator over decoded events. */ +async function openSse( + app: Hono, + path: string, + did: string | null = null +): Promise<{ + res: Response; + events: AsyncIterator; + close: () => void; +}> { + const ac = new AbortController(); + const headers: Record = {}; + if (did) headers["X-Test-Did"] = did; + const res = await app.fetch( + new Request(`http://localhost${path}`, { method: "GET", headers, signal: ac.signal }) + ); + if (!res.ok) { + throw new Error(`SSE open failed: ${res.status} ${await res.text()}`); + } + const reader = res.body!.getReader(); + const decoder = new TextDecoder(); + let buf = ""; + const iter: AsyncIterator = { + async next() { + // Read frames until we get an event with valid JSON data. + while (true) { + // Parse any complete frames already buffered. + while (true) { + const sep = buf.indexOf("\n\n"); + if (sep < 0) break; + const frame = buf.slice(0, sep); + buf = buf.slice(sep + 2); + const lines = frame.split("\n"); + let data: string | null = null; + for (const line of lines) { + if (line.startsWith(":")) continue; // comment / keepalive + if (line.startsWith("data:")) data = line.slice(5).trim(); + } + if (data) { + return { value: JSON.parse(data) as RealtimeEvent, done: false }; + } + } + const r = await reader.read(); + if (r.done) return { value: undefined, done: true }; + buf += decoder.decode(r.value, { stream: true }); + } + }, + async return() { + ac.abort(); + try { await reader.cancel(); } catch { /* */ } + return { value: undefined, done: true }; + }, + }; + return { res, events: iter, close: () => { ac.abort(); reader.cancel().catch(() => {}); } }; +} + +async function createSpace(app: Hono, owner: string, key?: string): Promise { + const res = await call(app, "POST", "/xrpc/test.rt.space.createSpace", owner, { key }); + expect(res.status).toBe(200); + return ((await res.json()) as any).space.uri; +} + +async function mintTicket(app: Hono, did: string, topic: string): Promise<{ ticket: string; topics: string[] }> { + const res = await call(app, "POST", "/xrpc/test.rt.realtime.ticket", did, { topic }); + expect(res.status).toBe(200); + return (await res.json()) as any; +} + +describe("realtime e2e (in-memory pubsub, SSE transport)", () => { + let app: Hono; + + beforeAll(async () => { + app = await makeApp(); + }); + + it("ticket+SSE: owner subscribes and receives record.created", async () => { + const spaceUri = await createSpace(app, ALICE); + const { ticket } = await mintTicket(app, ALICE, `space:${spaceUri}`); + const { events, close } = await openSse( + app, + `/xrpc/test.rt.realtime.subscribe?ticket=${encodeURIComponent(ticket)}` + ); + // Publish a record. + const put = await call(app, "POST", "/xrpc/test.rt.space.putRecord", ALICE, { + spaceUri, + collection: "app.event.message", + record: { text: "hello" }, + }); + expect(put.status).toBe(200); + const next = await events.next(); + expect(next.done).toBe(false); + expect((next.value as RealtimeEvent).kind).toBe("record.created"); + close(); + }); + + it("non-member cannot mint a ticket for a space they don't belong to", async () => { + const spaceUri = await createSpace(app, ALICE, "members-only"); + const res = await call(app, "POST", "/xrpc/test.rt.realtime.ticket", BOB, { + topic: `space:${spaceUri}`, + }); + expect(res.status).toBe(403); + }); + + it("JWT path (bot): subscribe without a ticket using Authorization-equivalent", async () => { + const spaceUri = await createSpace(app, ALICE, "bot-space"); + const { events, close } = await openSse( + app, + `/xrpc/test.rt.realtime.subscribe?topic=${encodeURIComponent("space:" + spaceUri)}`, + ALICE + ); + await call(app, "POST", "/xrpc/test.rt.space.putRecord", ALICE, { + spaceUri, + collection: "app.event.message", + record: { text: "from-bot" }, + }); + const next = await events.next(); + expect((next.value as RealtimeEvent).kind).toBe("record.created"); + close(); + }); + + it("collection filter drops non-matching record events", async () => { + const spaceUri = await createSpace(app, ALICE, "filter-space"); + const { events, close } = await openSse( + app, + `/xrpc/test.rt.realtime.subscribe?topic=${encodeURIComponent("space:" + spaceUri)}&collection=app.event.message`, + ALICE + ); + // Write one matching record — should arrive. + await call(app, "POST", "/xrpc/test.rt.space.putRecord", ALICE, { + spaceUri, + collection: "app.event.message", + record: { text: "match" }, + }); + const ev = await events.next(); + expect((ev.value as RealtimeEvent).kind).toBe("record.created"); + expect((ev.value as RealtimeEvent & { kind: "record.created" }).payload.collection).toBe( + "app.event.message" + ); + close(); + }); + + it("member.removed kicks the subscriber's stream", async () => { + const spaceUri = await createSpace(app, ALICE, "kick-space"); + // Add Bob as a member. + await call(app, "POST", "/xrpc/test.rt.space.addMember", ALICE, { spaceUri, did: BOB }); + const { ticket } = await mintTicket(app, BOB, `space:${spaceUri}`); + const { events, close } = await openSse( + app, + `/xrpc/test.rt.realtime.subscribe?ticket=${encodeURIComponent(ticket)}` + ); + // Remove Bob. + const rm = await call(app, "POST", "/xrpc/test.rt.space.removeMember", ALICE, { + spaceUri, + did: BOB, + }); + expect(rm.status).toBe(200); + // The next event Bob receives should be his own member.removed, and then + // the stream should close. + const ev = await events.next(); + expect(ev.done).toBe(false); + expect((ev.value as RealtimeEvent).kind).toBe("member.removed"); + const done = await events.next(); + expect(done.done).toBe(true); + close(); + }); + + it("invalid/expired ticket is rejected", async () => { + const res = await app.fetch( + new Request(`http://localhost/xrpc/test.rt.realtime.subscribe?ticket=bad.blob`, { + method: "GET", + }) + ); + expect(res.status).toBe(401); + }); + + it("community: alias expands to reachable spaces", async () => { + // Adopt a community, create two child spaces, grant Charlie member on one. + const adoptRes = await call(app, "POST", "/xrpc/test.rt.community.adopt", ALICE, { + identifier: "did:plc:community", + appPassword: "anything", // mockFetch returns 200 for createSession + }); + expect(adoptRes.status).toBe(200); + const { communityDid } = (await adoptRes.json()) as any; + + const c1 = await call(app, "POST", "/xrpc/test.rt.community.space.create", ALICE, { + communityDid, + key: "general", + }); + expect(c1.status).toBe(200); + const general = ((await c1.json()) as any).space.uri as string; + + // Grant Charlie as member in general. + const g = await call(app, "POST", "/xrpc/test.rt.community.space.grant", ALICE, { + spaceUri: general, + subject: { did: CHARLIE }, + accessLevel: "member", + }); + expect(g.status).toBe(200); + + // Charlie mints a community-alias ticket; should expand to [space:general]. + const ticketRes = await call(app, "POST", "/xrpc/test.rt.realtime.ticket", CHARLIE, { + topic: `community:${communityDid}`, + }); + expect(ticketRes.status).toBe(200); + const body = (await ticketRes.json()) as any; + expect(body.topics).toContain(`space:${general}`); + }); +}); diff --git a/tests/realtime-foundations.test.ts b/tests/realtime-foundations.test.ts new file mode 100644 index 0000000..b7ca223 --- /dev/null +++ b/tests/realtime-foundations.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect } from "vitest"; +import { InMemoryPubSub } from "../src/core/realtime/in-memory"; +import { TicketSigner } from "../src/core/realtime/ticket"; +import type { RealtimeEvent } from "../src/core/realtime/types"; +import { spaceTopic } from "../src/core/realtime/types"; + +const SECRET = new Uint8Array(32).fill(3); + +function mk(topic: string, n: number): RealtimeEvent { + return { + topic, + kind: "record.created", + payload: { + spaceUri: "at://x/y/z", + collection: "c", + authorDid: "did:plc:x", + rkey: String(n), + }, + ts: n, + }; +} + +describe("InMemoryPubSub", () => { + it("delivers events to a subscriber on the same topic", async () => { + const ps = new InMemoryPubSub(); + const topic = spaceTopic("at://x/y/z"); + const it = ps.subscribe(topic)[Symbol.asyncIterator](); + await ps.publish(mk(topic, 1)); + const r = await it.next(); + expect(r.done).toBe(false); + expect((r.value as RealtimeEvent).ts).toBe(1); + await it.return?.(); + }); + + it("fans out to multiple subscribers on the same topic", async () => { + const ps = new InMemoryPubSub(); + const topic = spaceTopic("at://x/y/z"); + const a = ps.subscribe(topic)[Symbol.asyncIterator](); + const b = ps.subscribe(topic)[Symbol.asyncIterator](); + await ps.publish(mk(topic, 42)); + const [ra, rb] = await Promise.all([a.next(), b.next()]); + expect((ra.value as RealtimeEvent).ts).toBe(42); + expect((rb.value as RealtimeEvent).ts).toBe(42); + await a.return?.(); + await b.return?.(); + }); + + it("does not cross topics", async () => { + const ps = new InMemoryPubSub(); + const topic = spaceTopic("at://x/y/z"); + const other = spaceTopic("at://x/y/other"); + const it = ps.subscribe(topic)[Symbol.asyncIterator](); + await ps.publish(mk(other, 99)); + await ps.publish(mk(topic, 1)); + const r = await it.next(); + expect((r.value as RealtimeEvent).ts).toBe(1); + await it.return?.(); + }); + + it("drops oldest when queue bound is reached", async () => { + const ps = new InMemoryPubSub({ queueBound: 2 }); + const topic = spaceTopic("at://x/y/z"); + const it = ps.subscribe(topic)[Symbol.asyncIterator](); + // Fill past the bound before the consumer starts reading. + for (let i = 1; i <= 5; i++) await ps.publish(mk(topic, i)); + const seen: number[] = []; + for (let i = 0; i < 2; i++) { + const r = await it.next(); + seen.push((r.value as RealtimeEvent).ts); + } + // Queue bound = 2: the last two events survive (3 was dropped before 4, etc.). + expect(seen).toEqual([4, 5]); + await it.return?.(); + }); + + it("cleans up subscribers on return()", async () => { + const ps = new InMemoryPubSub(); + const topic = spaceTopic("at://x/y/z"); + const it = ps.subscribe(topic)[Symbol.asyncIterator](); + expect(ps.subscriberCount(topic)).toBe(1); + await it.return?.(); + expect(ps.subscriberCount(topic)).toBe(0); + }); + + it("cleans up subscribers when the abort signal fires", async () => { + const ps = new InMemoryPubSub(); + const topic = spaceTopic("at://x/y/z"); + const ac = new AbortController(); + const iter = ps.subscribe(topic, ac.signal); + const it = iter[Symbol.asyncIterator](); + // Start a pending read, then abort. + const next = it.next(); + ac.abort(); + const r = await next; + expect(r.done).toBe(true); + expect(ps.subscriberCount(topic)).toBe(0); + }); +}); + +describe("TicketSigner", () => { + it("round-trips a signed ticket", async () => { + const signer = new TicketSigner(SECRET); + const t = await signer.sign({ topics: ["space:a", "space:b"], did: "did:plc:bob", ttlMs: 60_000 }); + const payload = await signer.verify(t); + expect(payload).not.toBeNull(); + expect(payload!.did).toBe("did:plc:bob"); + expect(payload!.topics).toEqual(["space:a", "space:b"]); + }); + + it("rejects a tampered payload", async () => { + const signer = new TicketSigner(SECRET); + const t = await signer.sign({ topics: ["space:a"], did: "did:plc:bob", ttlMs: 60_000 }); + // Replace payload half while keeping original sig — corrupt. + const tampered = t.replace(/^[^.]+/, "AAAA"); + expect(await signer.verify(tampered)).toBeNull(); + }); + + it("rejects signatures from a different secret", async () => { + const a = new TicketSigner(SECRET); + const b = new TicketSigner(new Uint8Array(32).fill(4)); + const t = await a.sign({ topics: ["space:a"], did: "did:plc:bob", ttlMs: 60_000 }); + expect(await b.verify(t)).toBeNull(); + }); + + it("rejects expired tickets", async () => { + const signer = new TicketSigner(SECRET); + const t = await signer.sign({ topics: ["space:a"], did: "did:plc:bob", ttlMs: -1 }); + expect(await signer.verify(t)).toBeNull(); + }); + + it("rejects malformed tickets", async () => { + const signer = new TicketSigner(SECRET); + expect(await signer.verify("")).toBeNull(); + expect(await signer.verify("nodot")).toBeNull(); + expect(await signer.verify("abc.def")).toBeNull(); + }); + + it("accepts secrets as base64 / hex strings", async () => { + const hex = "03".repeat(32); + const signer = new TicketSigner(hex); + const t = await signer.sign({ topics: ["x"], did: "d", ttlMs: 60_000 }); + expect(await signer.verify(t)).not.toBeNull(); + }); +}); -- 2.51.2 From 70523d2934c0c6f2c63acc8297b9b93dc8fae17c Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:29:59 +0200 Subject: [PATCH 04/18] blobs --- lexicons-generated/rsvp/atmo/space/defs.json | 29 ++ .../rsvp/atmo/space/getBlob.json | 42 +++ .../rsvp/atmo/space/listBlobs.json | 57 +++ .../rsvp/atmo/space/uploadBlob.json | 50 +++ spaces-lexicon-templates/defs.json | 11 + spaces-lexicon-templates/getBlob.json | 27 ++ spaces-lexicon-templates/listBlobs.json | 31 ++ spaces-lexicon-templates/uploadBlob.json | 33 ++ src/core/spaces/adapter.ts | 124 +++++++ src/core/spaces/blob-adapter.ts | 94 +++++ src/core/spaces/blob-gc.ts | 38 ++ src/core/spaces/blob-refs.ts | 27 ++ src/core/spaces/router.ts | 181 +++++++++- src/core/spaces/schema.ts | 12 + src/core/spaces/types.ts | 48 +++ tests/spaces-blobs.test.ts | 325 ++++++++++++++++++ 16 files changed, 1128 insertions(+), 1 deletion(-) create mode 100644 lexicons-generated/rsvp/atmo/space/getBlob.json create mode 100644 lexicons-generated/rsvp/atmo/space/listBlobs.json create mode 100644 lexicons-generated/rsvp/atmo/space/uploadBlob.json create mode 100644 spaces-lexicon-templates/getBlob.json create mode 100644 spaces-lexicon-templates/listBlobs.json create mode 100644 spaces-lexicon-templates/uploadBlob.json create mode 100644 src/core/spaces/blob-adapter.ts create mode 100644 src/core/spaces/blob-gc.ts create mode 100644 src/core/spaces/blob-refs.ts create mode 100644 tests/spaces-blobs.test.ts diff --git a/lexicons-generated/rsvp/atmo/space/defs.json b/lexicons-generated/rsvp/atmo/space/defs.json index 01f50b6..a9f68e3 100644 --- a/lexicons-generated/rsvp/atmo/space/defs.json +++ b/lexicons-generated/rsvp/atmo/space/defs.json @@ -127,6 +127,35 @@ } } }, + "blobInfo": { + "type": "object", + "required": [ + "cid", + "mimeType", + "size", + "authorDid", + "createdAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + } + } + }, "inviteView": { "type": "object", "required": [ diff --git a/lexicons-generated/rsvp/atmo/space/getBlob.json b/lexicons-generated/rsvp/atmo/space/getBlob.json new file mode 100644 index 0000000..a4b45c5 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/getBlob.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.getBlob", + "defs": { + "main": { + "type": "query", + "description": "Read a blob from a space. Requires read access via a service-auth JWT or a read-grant invite token.", + "parameters": { + "type": "params", + "required": [ + "spaceUri", + "cid" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access." + } + } + }, + "output": { + "encoding": "*/*" + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/listBlobs.json b/lexicons-generated/rsvp/atmo/space/listBlobs.json new file mode 100644 index 0000000..b19d743 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/listBlobs.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.listBlobs", + "defs": { + "main": { + "type": "query", + "description": "List blob metadata for a space. Members only.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only blobs uploaded by this DID." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "blobs" + ], + "properties": { + "blobs": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#blobInfo" + } + }, + "cursor": { + "type": "string" + } + } + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/uploadBlob.json b/lexicons-generated/rsvp/atmo/space/uploadBlob.json new file mode 100644 index 0000000..c7517dc --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/uploadBlob.json @@ -0,0 +1,50 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.uploadBlob", + "defs": { + "main": { + "type": "procedure", + "description": "Upload a blob into a space. Returns a standard atproto BlobRef that records in this space can then reference. The record will be rejected at putRecord time if it references a blob that was not uploaded to the same space.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "input": { + "encoding": "*/*" + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "blob" + ], + "properties": { + "blob": { + "type": "blob" + } + } + } + }, + "errors": [ + { + "name": "Forbidden" + }, + { + "name": "BlobTooLarge" + }, + { + "name": "InvalidMimeType" + } + ] + } + } +} diff --git a/spaces-lexicon-templates/defs.json b/spaces-lexicon-templates/defs.json index 4eaee52..2d5c9cf 100644 --- a/spaces-lexicon-templates/defs.json +++ b/spaces-lexicon-templates/defs.json @@ -47,6 +47,17 @@ "apps": { "type": "array", "items": { "type": "string" } } } }, + "blobInfo": { + "type": "object", + "required": ["cid", "mimeType", "size", "authorDid", "createdAt"], + "properties": { + "cid": { "type": "string", "format": "cid" }, + "mimeType": { "type": "string" }, + "size": { "type": "integer" }, + "authorDid": { "type": "string", "format": "did" }, + "createdAt": { "type": "integer" } + } + }, "inviteView": { "type": "object", "required": ["tokenHash", "spaceUri", "kind", "usedCount", "createdBy", "createdAt"], diff --git a/spaces-lexicon-templates/getBlob.json b/spaces-lexicon-templates/getBlob.json new file mode 100644 index 0000000..b866979 --- /dev/null +++ b/spaces-lexicon-templates/getBlob.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.getBlob", + "defs": { + "main": { + "type": "query", + "description": "Read a blob from a space. Requires read access via a service-auth JWT or a read-grant invite token.", + "parameters": { + "type": "params", + "required": ["spaceUri", "cid"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access." + } + } + }, + "output": { "encoding": "*/*" }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/spaces-lexicon-templates/listBlobs.json b/spaces-lexicon-templates/listBlobs.json new file mode 100644 index 0000000..ce7afd0 --- /dev/null +++ b/spaces-lexicon-templates/listBlobs.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.listBlobs", + "defs": { + "main": { + "type": "query", + "description": "List blob metadata for a space. Members only.", + "parameters": { + "type": "params", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "byUser": { "type": "string", "format": "did", "description": "Only blobs uploaded by this DID." }, + "limit": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }, + "cursor": { "type": "string" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["blobs"], + "properties": { + "blobs": { "type": "array", "items": { "type": "ref", "ref": "tools.atmo.space.defs#blobInfo" } }, + "cursor": { "type": "string" } + } + } + } + } + } +} diff --git a/spaces-lexicon-templates/uploadBlob.json b/spaces-lexicon-templates/uploadBlob.json new file mode 100644 index 0000000..53a68f6 --- /dev/null +++ b/spaces-lexicon-templates/uploadBlob.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.uploadBlob", + "defs": { + "main": { + "type": "procedure", + "description": "Upload a blob into a space. Returns a standard atproto BlobRef that records in this space can then reference. The record will be rejected at putRecord time if it references a blob that was not uploaded to the same space.", + "parameters": { + "type": "params", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" } + } + }, + "input": { "encoding": "*/*" }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["blob"], + "properties": { + "blob": { "type": "blob" } + } + } + }, + "errors": [ + { "name": "Forbidden" }, + { "name": "BlobTooLarge" }, + { "name": "InvalidMimeType" } + ] + } + } +} diff --git a/src/core/spaces/adapter.ts b/src/core/spaces/adapter.ts index 8c839d3..b5f6b3c 100644 --- a/src/core/spaces/adapter.ts +++ b/src/core/spaces/adapter.ts @@ -10,10 +10,13 @@ import { import { getDialect } from "../dialect"; import type { AppPolicy, + BlobMetaRow, CollectionCount, CreateInviteInput, InviteKind, InviteRow, + ListBlobsOptions, + ListBlobsResult, ListOptions, ListResult, ListSpacesOptions, @@ -63,6 +66,17 @@ function mapMemberRow(row: any): SpaceMemberRow { }; } +function mapBlobMetaRow(row: any): BlobMetaRow { + return { + spaceUri: row.space_uri, + cid: row.cid, + mimeType: row.mime_type, + size: Number(row.size), + authorDid: row.author_did, + createdAt: toNum(row.created_at), + }; +} + function mapInviteRow(row: any): InviteRow { return { tokenHash: row.token_hash, @@ -601,4 +615,114 @@ export class HostedAdapter implements StorageAdapter { } return results; } + + async putBlobMeta(row: BlobMetaRow): Promise { + const sql = `INSERT INTO spaces_blobs (space_uri, cid, mime_type, size, author_did, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (space_uri, cid) DO NOTHING`; + await this.db + .prepare(sql) + .bind(row.spaceUri, row.cid, row.mimeType, row.size, row.authorDid, row.createdAt) + .run(); + } + + async getBlobMeta(spaceUri: string, cid: string): Promise { + const r = await this.db + .prepare(`SELECT * FROM spaces_blobs WHERE space_uri = ? AND cid = ?`) + .bind(spaceUri, cid) + .first(); + return r ? mapBlobMetaRow(r) : null; + } + + async listBlobMeta( + spaceUri: string, + options: ListBlobsOptions = {} + ): Promise { + const limit = Math.min(options.limit ?? 50, 200); + const clauses: string[] = ["space_uri = ?"]; + const params: any[] = [spaceUri]; + if (options.byUser) { + clauses.push("author_did = ?"); + params.push(options.byUser); + } + if (options.cursor) { + clauses.push("created_at < ?"); + params.push(Number(options.cursor)); + } + const sql = `SELECT * FROM spaces_blobs + WHERE ${clauses.join(" AND ")} + ORDER BY created_at DESC + LIMIT ?`; + params.push(limit + 1); + const { results } = await this.db.prepare(sql).bind(...params).all(); + const blobs = results.map(mapBlobMetaRow); + let cursor: string | undefined; + if (blobs.length > limit) { + const next = blobs.pop()!; + cursor = String(next.createdAt); + } + return { blobs, cursor }; + } + + async deleteBlobMeta(spaceUri: string, cid: string): Promise { + await this.db + .prepare(`DELETE FROM spaces_blobs WHERE space_uri = ? AND cid = ?`) + .bind(spaceUri, cid) + .run(); + } + + async findOrphanBlobs( + spaceUri: string, + cutoff: number, + limit: number + ): Promise { + if (!this.config) return []; + // Gather candidate blobs older than cutoff, then filter out any whose CID + // appears in any record JSON in this space. We use a cheap substring probe + // (LIKE) per collection — false positives are OK because an orphan that + // survives GC just gets collected next cycle; false negatives (deleting + // a referenced blob) would be a bug, and substring search over the full + // CID is safe enough for that. + const { results } = await this.db + .prepare( + `SELECT * FROM spaces_blobs + WHERE space_uri = ? AND created_at < ? + ORDER BY created_at ASC + LIMIT ?` + ) + .bind(spaceUri, cutoff, limit) + .all(); + const candidates = results.map(mapBlobMetaRow); + if (candidates.length === 0) return []; + + const tables: string[] = []; + for (const [short, colConfig] of Object.entries(this.config.collections)) { + if (colConfig.allowInSpaces === false) continue; + tables.push(spacesRecordsTableName(short)); + } + + const orphans: BlobMetaRow[] = []; + for (const blob of candidates) { + let referenced = false; + const pattern = `%${blob.cid}%`; + for (const table of tables) { + try { + const row = await this.db + .prepare( + `SELECT 1 FROM ${table} WHERE space_uri = ? AND record LIKE ? LIMIT 1` + ) + .bind(spaceUri, pattern) + .first(); + if (row) { + referenced = true; + break; + } + } catch { + // table missing — ignore + } + } + if (!referenced) orphans.push(blob); + } + return orphans; + } } diff --git a/src/core/spaces/blob-adapter.ts b/src/core/spaces/blob-adapter.ts new file mode 100644 index 0000000..632b566 --- /dev/null +++ b/src/core/spaces/blob-adapter.ts @@ -0,0 +1,94 @@ +/** + * Bytes-only storage adapter for space blobs. Metadata (CID, mime, size, + * author, space) lives in the `spaces_blobs` table on the main StorageAdapter; + * this interface only moves bytes in and out of a backend (R2, S3, fs, …). + * + * Keys are opaque strings formed by the router as `blobKey(spaceUri, cid)`. + */ + +export interface BlobUploadMeta { + mimeType: string; + size: number; +} + +export interface BlobAdapter { + put(key: string, bytes: Uint8Array, meta: BlobUploadMeta): Promise; + get(key: string): Promise; + /** Bulk delete. Adapters that don't support batch can implement serially. */ + delete(keys: string[]): Promise; +} + +/** In-memory adapter. Useful for tests and local development. */ +export class MemoryBlobAdapter implements BlobAdapter { + private readonly store = new Map(); + + async put(key: string, bytes: Uint8Array): Promise { + this.store.set(key, bytes.slice()); + } + + async get(key: string): Promise { + const v = this.store.get(key); + return v ? v.slice() : null; + } + + async delete(keys: string[]): Promise { + for (const k of keys) this.store.delete(k); + } + + /** Test helper. */ + size(): number { + return this.store.size; + } +} + +/** Minimal Cloudflare R2 bucket shape — matches @cloudflare/workers-types' R2Bucket + * without forcing a types dependency here. */ +export interface R2BucketLike { + put( + key: string, + value: ArrayBuffer | ArrayBufferView | ReadableStream | Blob, + options?: { httpMetadata?: { contentType?: string }; customMetadata?: Record } + ): Promise; + get(key: string): Promise<{ arrayBuffer(): Promise } | null>; + delete(keys: string | string[]): Promise; +} + +/** Cloudflare R2 adapter. Pass the `env.BLOBS` binding from your Worker. */ +export class R2BlobAdapter implements BlobAdapter { + constructor(private readonly bucket: R2BucketLike) {} + + async put(key: string, bytes: Uint8Array, meta: BlobUploadMeta): Promise { + await this.bucket.put(key, bytes, { + httpMetadata: { contentType: meta.mimeType }, + }); + } + + async get(key: string): Promise { + const obj = await this.bucket.get(key); + if (!obj) return null; + const buf = await obj.arrayBuffer(); + return new Uint8Array(buf); + } + + async delete(keys: string[]): Promise { + if (keys.length === 0) return; + await this.bucket.delete(keys); + } +} + +/** Hash a space URI to a short, filesystem/R2-safe key segment. + * Used as the first segment of a blob key so all blobs for one space + * share a common prefix (enables bulk delete on space deletion). */ +export async function spaceKeyPrefix(spaceUri: string): Promise { + const bytes = new TextEncoder().encode(spaceUri); + const digest = await crypto.subtle.digest("SHA-256", bytes); + const hex = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join(""); + return hex.slice(0, 16); +} + +/** Compose an adapter key from a space URI and CID. + * Shape: `<16-hex-chars-of-sha256(spaceUri)>/`. */ +export async function blobKey(spaceUri: string, cid: string): Promise { + const prefix = await spaceKeyPrefix(spaceUri); + return `${prefix}/${cid}`; +} diff --git a/src/core/spaces/blob-gc.ts b/src/core/spaces/blob-gc.ts new file mode 100644 index 0000000..d523fcd --- /dev/null +++ b/src/core/spaces/blob-gc.ts @@ -0,0 +1,38 @@ +import type { BlobAdapter } from "./blob-adapter"; +import { blobKey } from "./blob-adapter"; +import type { StorageAdapter } from "./types"; + +export interface BlobGcOptions { + /** Orphan rows created before this timestamp are eligible for deletion. */ + olderThan: number; + /** Maximum number of blobs to delete in this pass. Defaults to 500. */ + batchSize?: number; +} + +export interface BlobGcResult { + deleted: number; + cids: string[]; +} + +/** Delete blob bytes + metadata for any blob older than `olderThan` that + * is not referenced by any record in the space. Safe to run periodically. */ +export async function gcOrphanBlobs( + storage: StorageAdapter, + blobs: BlobAdapter, + spaceUri: string, + options: BlobGcOptions +): Promise { + const batchSize = options.batchSize ?? 500; + const orphans = await storage.findOrphanBlobs(spaceUri, options.olderThan, batchSize); + if (orphans.length === 0) return { deleted: 0, cids: [] }; + + const keys: string[] = []; + for (const row of orphans) { + keys.push(await blobKey(row.spaceUri, row.cid)); + } + await blobs.delete(keys); + for (const row of orphans) { + await storage.deleteBlobMeta(row.spaceUri, row.cid); + } + return { deleted: orphans.length, cids: orphans.map((o) => o.cid) }; +} diff --git a/src/core/spaces/blob-refs.ts b/src/core/spaces/blob-refs.ts new file mode 100644 index 0000000..62cf693 --- /dev/null +++ b/src/core/spaces/blob-refs.ts @@ -0,0 +1,27 @@ +/** + * Walk a record JSON and collect every atproto blob ref. + * + * Blob refs look like: + * { "$type": "blob", "ref": { "$link": "" }, "mimeType": "...", "size": N } + * + * We return the CID strings. + */ +export function collectBlobCids(value: unknown, out: Set = new Set()): Set { + if (value == null) return out; + if (Array.isArray(value)) { + for (const v of value) collectBlobCids(v, out); + return out; + } + if (typeof value !== "object") return out; + + const obj = value as Record; + if (obj["$type"] === "blob") { + const ref = obj["ref"] as { $link?: unknown } | undefined; + if (ref && typeof ref["$link"] === "string") out.add(ref["$link"]); + // Don't descend — a blob ref's own shape has no nested blobs. + return out; + } + + for (const v of Object.values(obj)) collectBlobCids(v, out); + return out; +} diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index c1c102c..286c81f 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -13,7 +13,17 @@ import { import { nextTid } from "./tid"; import { generateInviteToken, hashInviteToken } from "./invite-token"; import { buildSpaceUri } from "./uri"; -import type { InviteKind, InviteRow, SpaceRow, SpacesConfig, StorageAdapter } from "./types"; +import { + DEFAULT_BLOB_MAX_SIZE, + type InviteKind, + type InviteRow, + type SpaceRow, + type SpacesConfig, + type StorageAdapter, +} from "./types"; +import { blobKey } from "./blob-adapter"; +import { collectBlobCids } from "./blob-refs"; +import { create as createCid, toString as cidToString } from "@atcute/cid"; import type { Did } from "@atcute/lexicons"; export interface SpacesRoutesOptions { @@ -230,6 +240,27 @@ export function registerSpacesRoutes( }); if (!result.allow) return c.json({ error: "Forbidden", reason: result.reason }, 403); + // Validate that every blob referenced by this record has already been + // uploaded into this space. This mirrors how PDSes require uploadBlob + // before putRecord, and prevents forging refs to blobs the caller never + // actually claimed. + if (spacesConfig.blobs) { + const cids = collectBlobCids(body.record); + for (const cid of cids) { + const meta = await adapter.getBlobMeta(body.spaceUri, cid); + if (!meta) { + return c.json( + { + error: "InvalidRequest", + reason: "unknown-blob-ref", + message: `Record references blob ${cid} that has not been uploaded to this space.`, + }, + 400 + ); + } + } + } + const rkey = body.rkey ?? nextTid(); const now = Date.now(); await adapter.putRecord({ @@ -270,6 +301,154 @@ export function registerSpacesRoutes( return c.json({ ok: true }); }); + // Blobs (only registered when a blob adapter is configured) + if (spacesConfig.blobs) { + const blobsCfg = spacesConfig.blobs; + const blobAdapter = blobsCfg.adapter; + const maxSize = blobsCfg.maxSize ?? DEFAULT_BLOB_MAX_SIZE; + const accept = blobsCfg.accept; + + app.post(`/xrpc/${SPACE}.uploadBlob`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + if (!spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const space = await adapter.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const member = await adapter.getMember(spaceUri, sa.issuer); + const aclResult = checkAccess({ + op: "write", + space, + callerDid: sa.issuer, + member, + clientId: sa.clientId, + }); + if (!aclResult.allow) { + return c.json({ error: "Forbidden", reason: aclResult.reason }, 403); + } + + const mimeType = c.req.header("content-type") ?? "application/octet-stream"; + if (accept && !accept.includes(mimeType)) { + return c.json( + { error: "InvalidMimeType", message: `MIME type ${mimeType} is not accepted.` }, + 400 + ); + } + + const declaredLen = c.req.header("content-length"); + if (declaredLen && Number(declaredLen) > maxSize) { + return c.json( + { error: "BlobTooLarge", message: `Blob exceeds max size of ${maxSize} bytes.` }, + 413 + ); + } + + const buf = await c.req.arrayBuffer(); + const bytes = new Uint8Array(buf); + if (bytes.byteLength > maxSize) { + return c.json( + { error: "BlobTooLarge", message: `Blob exceeds max size of ${maxSize} bytes.` }, + 413 + ); + } + + const cid = await createCid(0x55, bytes); + const cidString = cidToString(cid); + const key = await blobKey(spaceUri, cidString); + + await blobAdapter.put(key, bytes, { mimeType, size: bytes.byteLength }); + await adapter.putBlobMeta({ + spaceUri, + cid: cidString, + mimeType, + size: bytes.byteLength, + authorDid: sa.issuer, + createdAt: Date.now(), + }); + + return c.json({ + blob: { + $type: "blob", + ref: { $link: cidString }, + mimeType, + size: bytes.byteLength, + }, + }); + }); + + app.get(`/xrpc/${SPACE}.getBlob`, readAuth, async (c) => { + const spaceUri = c.req.query("spaceUri"); + const cid = c.req.query("cid"); + if (!spaceUri || !cid) { + return c.json({ error: "InvalidRequest", message: "spaceUri and cid required" }, 400); + } + const space = await adapter.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const authz = await authorizeRead(c, spaceUri); + if (authz instanceof Response) return authz; + + if (authz.via === "jwt") { + const sa = authz.sa; + const member = await adapter.getMember(spaceUri, sa.issuer); + const aclResult = checkAccess({ + op: "read", + space, + callerDid: sa.issuer, + member, + clientId: sa.clientId, + }); + if (!aclResult.allow) { + return c.json({ error: "Forbidden", reason: aclResult.reason }, 403); + } + } + + const meta = await adapter.getBlobMeta(spaceUri, cid); + if (!meta) return c.json({ error: "NotFound" }, 404); + const key = await blobKey(spaceUri, cid); + const bytes = await blobAdapter.get(key); + if (!bytes) return c.json({ error: "NotFound" }, 404); + + return new Response(bytes, { + headers: { + "content-type": meta.mimeType, + "content-length": String(meta.size), + }, + }); + }); + + app.get(`/xrpc/${SPACE}.listBlobs`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + if (!spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const space = await adapter.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const member = await adapter.getMember(spaceUri, sa.issuer); + const aclResult = checkAccess({ + op: "read", + space, + callerDid: sa.issuer, + member, + clientId: sa.clientId, + }); + if (!aclResult.allow) { + return c.json({ error: "Forbidden", reason: aclResult.reason }, 403); + } + + const result = await adapter.listBlobMeta(spaceUri, { + byUser: c.req.query("byUser") ?? undefined, + cursor: c.req.query("cursor") ?? undefined, + limit: c.req.query("limit") ? Number(c.req.query("limit")) : undefined, + }); + return c.json(result); + }); + } + // Space management (owner-gated) app.post(`/xrpc/${SPACE}.createSpace`, auth, async (c) => { const sa = getAuth(c); diff --git a/src/core/spaces/schema.ts b/src/core/spaces/schema.ts index d6cf4a1..a08ada6 100644 --- a/src/core/spaces/schema.ts +++ b/src/core/spaces/schema.ts @@ -34,6 +34,18 @@ export function buildSpacesBaseSchema(dialect: SqlDialect): string[] { )`, `CREATE INDEX IF NOT EXISTS idx_spaces_members_did ON spaces_members(did)`, + `CREATE TABLE IF NOT EXISTS spaces_blobs ( + space_uri TEXT NOT NULL, + cid TEXT NOT NULL, + mime_type TEXT NOT NULL, + size INTEGER NOT NULL, + author_did TEXT NOT NULL, + created_at ${dialect.bigintType} NOT NULL, + PRIMARY KEY (space_uri, cid) + )`, + `CREATE INDEX IF NOT EXISTS idx_spaces_blobs_author ON spaces_blobs(space_uri, author_did)`, + `CREATE INDEX IF NOT EXISTS idx_spaces_blobs_created ON spaces_blobs(space_uri, created_at)`, + `CREATE TABLE IF NOT EXISTS spaces_invites ( token_hash TEXT PRIMARY KEY, space_uri TEXT NOT NULL, diff --git a/src/core/spaces/types.ts b/src/core/spaces/types.ts index ff5584f..397e6a8 100644 --- a/src/core/spaces/types.ts +++ b/src/core/spaces/types.ts @@ -1,5 +1,6 @@ import type { Database } from "../types"; import type { DidDocumentResolver } from "@atcute/identity-resolver"; +import type { BlobAdapter } from "./blob-adapter"; export type AppPolicyMode = "allow" | "deny"; @@ -8,6 +9,22 @@ export interface AppPolicy { apps: string[]; } +export interface SpacesBlobsConfig { + /** Bytes backend (R2, S3, in-memory, …). */ + adapter: BlobAdapter; + /** Max blob size in bytes. Defaults to 2 MiB. */ + maxSize?: number; + /** MIME allowlist. If set, only these content types are accepted. */ + accept?: string[]; + /** Orphan blobs (those with no referencing record) are kept this long before + * GC can delete them, to allow upload-then-putRecord flows. + * Defaults to 24 hours. */ + gcOrphanAfterMs?: number; +} + +export const DEFAULT_BLOB_MAX_SIZE = 2 * 1024 * 1024; +export const DEFAULT_BLOB_GC_ORPHAN_AFTER_MS = 24 * 60 * 60 * 1000; + export interface SpacesConfig { /** NSID that identifies the kind of space this service hosts, e.g. "tools.atmo.event.space". */ type: string; @@ -18,6 +35,8 @@ export interface SpacesConfig { /** DID document resolver for service-auth JWT verification. * Defaults to a composite PLC + did:web resolver if omitted. */ resolver?: DidDocumentResolver; + /** Blob-upload backend. When omitted, blob XRPCs are not exposed. */ + blobs?: SpacesBlobsConfig; } export interface SpaceRow { @@ -106,6 +125,26 @@ export interface RedeemInviteResult { spaceUri: string; } +export interface BlobMetaRow { + spaceUri: string; + cid: string; + mimeType: string; + size: number; + authorDid: string; + createdAt: number; +} + +export interface ListBlobsOptions { + byUser?: string; + cursor?: string; + limit?: number; +} + +export interface ListBlobsResult { + blobs: BlobMetaRow[]; + cursor?: string; +} + export interface StorageAdapter { // Space lifecycle createSpace(space: Omit): Promise; @@ -144,6 +183,15 @@ export interface StorageAdapter { listRecords(spaceUri: string, collection: string, options?: ListOptions): Promise; deleteRecord(spaceUri: string, collection: string, authorDid: string, rkey: string): Promise; listCollections(spaceUri: string, options?: { byUser?: string }): Promise; + + // Blobs (metadata only; bytes live on BlobAdapter) + putBlobMeta(row: BlobMetaRow): Promise; + getBlobMeta(spaceUri: string, cid: string): Promise; + listBlobMeta(spaceUri: string, options?: ListBlobsOptions): Promise; + deleteBlobMeta(spaceUri: string, cid: string): Promise; + /** Find blob rows older than `cutoff` whose CIDs are not referenced in any + * record JSON in this space. Capped at `limit` to bound a single GC pass. */ + findOrphanBlobs(spaceUri: string, cutoff: number, limit: number): Promise; } export interface AdapterContext { diff --git a/tests/spaces-blobs.test.ts b/tests/spaces-blobs.test.ts new file mode 100644 index 0000000..63af7a5 --- /dev/null +++ b/tests/spaces-blobs.test.ts @@ -0,0 +1,325 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; +import { MemoryBlobAdapter } from "../src/core/spaces/blob-adapter"; +import { HostedAdapter } from "../src/core/spaces/adapter"; +import { gcOrphanBlobs } from "../src/core/spaces/blob-gc"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; +const CHARLIE = "did:plc:charlie"; + +function makeConfig(blobs: MemoryBlobAdapter, maxSize = 2 * 1024 * 1024): ContrailConfig { + return { + namespace: "test.blobs", + collections: { + photo: { collection: "app.event.photo" }, + }, + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:test.example#svc", + blobs: { adapter: blobs, maxSize }, + }, + }; +} + +function fakeAuth(aud: string): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { + issuer: did, + audience: aud, + lxm: undefined, + clientId: c.req.header("X-Test-App") ?? undefined, + }); + await next(); + }; +} + +async function makeApp( + blobs: MemoryBlobAdapter, + maxSize = 2 * 1024 * 1024 +): Promise<{ app: Hono; db: any; config: ReturnType }> { + const db = createSqliteDatabase(":memory:"); + const cfg = makeConfig(blobs, maxSize); + const resolved = resolveConfig(cfg); + await initSchema(db, resolved); + const app = createApp(db, resolved, { + spaces: { authMiddleware: fakeAuth(cfg.spaces!.serviceDid) }, + }); + return { app, db, config: resolved }; +} + +function call( + app: Hono, + method: string, + path: string, + did: string, + body?: BodyInit, + contentType?: string +): Promise { + const headers: Record = { "X-Test-Did": did }; + if (contentType) headers["Content-Type"] = contentType; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body, + }) + ); +} + +async function callJson( + app: Hono, + method: string, + path: string, + did: string, + body?: any +): Promise { + return call( + app, + method, + path, + did, + body === undefined ? undefined : JSON.stringify(body), + body === undefined ? undefined : "application/json" + ); +} + +async function createSpace(app: Hono, owner: string, key: string): Promise { + const res = await callJson(app, "POST", "/xrpc/test.blobs.space.createSpace", owner, { key }); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + return body.space.uri; +} + +describe("spaces blobs", () => { + let app: Hono; + let blobs: MemoryBlobAdapter; + let spaceUri: string; + + beforeAll(async () => { + blobs = new MemoryBlobAdapter(); + const out = await makeApp(blobs); + app = out.app; + spaceUri = await createSpace(app, ALICE, "album"); + }); + + it("owner uploads a blob and gets back a valid BlobRef", async () => { + const bytes = new TextEncoder().encode("hello world"); + const res = await call( + app, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(spaceUri)}`, + ALICE, + bytes, + "image/png" + ); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(body.blob.$type).toBe("blob"); + expect(body.blob.mimeType).toBe("image/png"); + expect(body.blob.size).toBe(bytes.byteLength); + expect(typeof body.blob.ref.$link).toBe("string"); + expect(body.blob.ref.$link.startsWith("b")).toBe(true); + }); + + it("non-member cannot upload", async () => { + const bytes = new TextEncoder().encode("intruder"); + const res = await call( + app, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(spaceUri)}`, + BOB, + bytes, + "image/png" + ); + expect(res.status).toBe(403); + }); + + it("upload is rejected when bytes exceed maxSize", async () => { + const tiny = new MemoryBlobAdapter(); + const { app: smallApp } = await makeApp(tiny, 64); + const uri = await createSpace(smallApp, ALICE, "small"); + const bytes = new Uint8Array(128); + const res = await call( + smallApp, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(uri)}`, + ALICE, + bytes, + "application/octet-stream" + ); + expect(res.status).toBe(413); + }); + + it("getBlob returns bytes for a member, 403 for non-member, 404 for bogus cid", async () => { + const payload = new TextEncoder().encode("download me"); + const up = await call( + app, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(spaceUri)}`, + ALICE, + payload, + "text/plain" + ); + const { blob } = (await up.json()) as any; + const cid = blob.ref.$link; + + // Member (owner) — gets bytes back. + const okRes = await call( + app, + "GET", + `/xrpc/test.blobs.space.getBlob?spaceUri=${encodeURIComponent(spaceUri)}&cid=${cid}`, + ALICE + ); + expect(okRes.status).toBe(200); + expect(okRes.headers.get("content-type")).toBe("text/plain"); + const got = new Uint8Array(await okRes.arrayBuffer()); + expect(new TextDecoder().decode(got)).toBe("download me"); + + // Non-member — 403. + const deny = await call( + app, + "GET", + `/xrpc/test.blobs.space.getBlob?spaceUri=${encodeURIComponent(spaceUri)}&cid=${cid}`, + CHARLIE + ); + expect(deny.status).toBe(403); + + // Made-up CID — 404 even for the owner (never enumerate). + const miss = await call( + app, + "GET", + `/xrpc/test.blobs.space.getBlob?spaceUri=${encodeURIComponent(spaceUri)}&cid=bafynotareal`, + ALICE + ); + expect(miss.status).toBe(404); + }); + + it("putRecord rejects records referencing blobs that were not uploaded here", async () => { + const res = await callJson(app, "POST", "/xrpc/test.blobs.space.putRecord", ALICE, { + spaceUri, + collection: "app.event.photo", + record: { + caption: "bogus", + image: { + $type: "blob", + ref: { $link: "bafynotareal" }, + mimeType: "image/png", + size: 10, + }, + }, + }); + expect(res.status).toBe(400); + const body = (await res.json()) as any; + expect(body.reason).toBe("unknown-blob-ref"); + }); + + it("putRecord accepts records referencing a previously uploaded blob", async () => { + const bytes = new TextEncoder().encode("real image bytes"); + const up = await call( + app, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(spaceUri)}`, + ALICE, + bytes, + "image/png" + ); + const { blob } = (await up.json()) as any; + + const res = await callJson(app, "POST", "/xrpc/test.blobs.space.putRecord", ALICE, { + spaceUri, + collection: "app.event.photo", + record: { caption: "ok", image: blob }, + }); + expect(res.status).toBe(200); + }); + + it("listBlobs returns metadata for members", async () => { + const res = await call( + app, + "GET", + `/xrpc/test.blobs.space.listBlobs?spaceUri=${encodeURIComponent(spaceUri)}`, + ALICE + ); + expect(res.status).toBe(200); + const body = (await res.json()) as any; + expect(Array.isArray(body.blobs)).toBe(true); + expect(body.blobs.length).toBeGreaterThan(0); + const row = body.blobs[0]; + expect(row).toHaveProperty("cid"); + expect(row).toHaveProperty("mimeType"); + expect(row).toHaveProperty("size"); + expect(row).toHaveProperty("authorDid"); + }); + + it("gcOrphanBlobs deletes unreferenced blobs older than cutoff but keeps referenced ones", async () => { + const isolatedBlobs = new MemoryBlobAdapter(); + const { app: isoApp, db } = await makeApp(isolatedBlobs); + const uri = await createSpace(isoApp, ALICE, "gc-test"); + const storage = new HostedAdapter(db, makeConfig(isolatedBlobs)); + + // Upload two blobs; reference only one of them. + const keepBytes = new TextEncoder().encode("keep me"); + const dropBytes = new TextEncoder().encode("drop me"); + + const keepUp = await call( + isoApp, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(uri)}`, + ALICE, + keepBytes, + "image/png" + ); + const dropUp = await call( + isoApp, + "POST", + `/xrpc/test.blobs.space.uploadBlob?spaceUri=${encodeURIComponent(uri)}`, + ALICE, + dropBytes, + "image/png" + ); + const keep = (await keepUp.json()) as any; + const drop = (await dropUp.json()) as any; + + // Reference the "keep" blob from a record. + const put = await callJson(isoApp, "POST", "/xrpc/test.blobs.space.putRecord", ALICE, { + spaceUri: uri, + collection: "app.event.photo", + record: { caption: "keep", image: keep.blob }, + }); + expect(put.status).toBe(200); + + // Run GC with a cutoff far in the future so every blob is eligible by age. + const result = await gcOrphanBlobs(storage, isolatedBlobs, uri, { + olderThan: Date.now() + 60_000, + }); + + expect(result.deleted).toBe(1); + expect(result.cids).toContain(drop.blob.ref.$link); + expect(result.cids).not.toContain(keep.blob.ref.$link); + + // The "keep" blob is still retrievable; the dropped one is gone. + const okRes = await call( + isoApp, + "GET", + `/xrpc/test.blobs.space.getBlob?spaceUri=${encodeURIComponent(uri)}&cid=${keep.blob.ref.$link}`, + ALICE + ); + expect(okRes.status).toBe(200); + const missRes = await call( + isoApp, + "GET", + `/xrpc/test.blobs.space.getBlob?spaceUri=${encodeURIComponent(uri)}&cid=${drop.blob.ref.$link}`, + ALICE + ); + expect(missRes.status).toBe(404); + }); +}); -- 2.51.2 From a38a439bbaf03d6ca9c33f5a24b724dc40e5ad90 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:49:40 +0200 Subject: [PATCH 05/18] add record to payload --- src/core/realtime/publishing-adapter.ts | 3 +++ src/core/realtime/types.ts | 17 +++++++++++++++-- tests/realtime-durable-object.test.ts | 10 +++++++++- tests/realtime-e2e.test.ts | 7 ++++++- tests/realtime-foundations.test.ts | 3 +++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/core/realtime/publishing-adapter.ts b/src/core/realtime/publishing-adapter.ts index edffe0a..24c6e06 100644 --- a/src/core/realtime/publishing-adapter.ts +++ b/src/core/realtime/publishing-adapter.ts @@ -115,6 +115,9 @@ export function wrapWithPublishing( collection: record.collection, authorDid: record.authorDid, rkey: record.rkey, + cid: record.cid, + record: record.record, + createdAt: record.createdAt, }, ts: now, })); diff --git a/src/core/realtime/types.ts b/src/core/realtime/types.ts index c4aaff1..4f72dfd 100644 --- a/src/core/realtime/types.ts +++ b/src/core/realtime/types.ts @@ -1,11 +1,24 @@ /** Realtime module — canonical types + interfaces. See docs/realtime.md. */ -/** Discriminated union of every event kind that flows through the PubSub. */ +/** Discriminated union of every event kind that flows through the PubSub. + * + * `record.created` carries the full record body so a subscriber can apply an + * insert or upsert without a follow-up `getRecord` call. `putRecord` over an + * existing `(authorDid, rkey)` publishes another `record.created` — treat it + * as upsert. */ export type RealtimeEvent = | { topic: string; kind: "record.created"; - payload: { spaceUri: string; collection: string; authorDid: string; rkey: string }; + payload: { + spaceUri: string; + collection: string; + authorDid: string; + rkey: string; + cid: string | null; + record: Record; + createdAt: number; + }; ts: number; } | { diff --git a/tests/realtime-durable-object.test.ts b/tests/realtime-durable-object.test.ts index 237d184..0c79e34 100644 --- a/tests/realtime-durable-object.test.ts +++ b/tests/realtime-durable-object.test.ts @@ -51,7 +51,15 @@ function mkEvent(overrides: Partial = {}): RealtimeEvent { return { topic: "space:at://x/y/z", kind: "record.created", - payload: { spaceUri: "at://x/y/z", collection: "c", authorDid: "did:plc:x", rkey: "r" }, + payload: { + spaceUri: "at://x/y/z", + collection: "c", + authorDid: "did:plc:x", + rkey: "r", + cid: null, + record: {}, + createdAt: 1, + }, ts: 1, ...(overrides as any), }; diff --git a/tests/realtime-e2e.test.ts b/tests/realtime-e2e.test.ts index 431e1fa..df11f61 100644 --- a/tests/realtime-e2e.test.ts +++ b/tests/realtime-e2e.test.ts @@ -186,7 +186,12 @@ describe("realtime e2e (in-memory pubsub, SSE transport)", () => { expect(put.status).toBe(200); const next = await events.next(); expect(next.done).toBe(false); - expect((next.value as RealtimeEvent).kind).toBe("record.created"); + const event = next.value as RealtimeEvent & { kind: "record.created" }; + expect(event.kind).toBe("record.created"); + // Fat payload: subscribers can render the new record without a follow-up fetch. + expect(event.payload.record).toEqual({ text: "hello" }); + expect(event.payload.collection).toBe("app.event.message"); + expect(event.payload.authorDid).toBe(ALICE); close(); }); diff --git a/tests/realtime-foundations.test.ts b/tests/realtime-foundations.test.ts index b7ca223..9d814d8 100644 --- a/tests/realtime-foundations.test.ts +++ b/tests/realtime-foundations.test.ts @@ -15,6 +15,9 @@ function mk(topic: string, n: number): RealtimeEvent { collection: "c", authorDid: "did:plc:x", rkey: String(n), + cid: null, + record: {}, + createdAt: n, }, ts: n, }; -- 2.51.2 From 8cbdbcbc2623dd8d755a87d3f7c4c716e4f76a80 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:12:04 +0200 Subject: [PATCH 06/18] add helper for permissions --- src/generate.ts | 32 ++++++++++++++++++++++++++++++++ tests/generate.test.ts | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/generate.ts b/src/generate.ts index dfc838a..c56fa6b 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -9,6 +9,38 @@ import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync, readdirSync import { join } from "path"; import type { ContrailConfig } from "./core/types"; +/** Return the sorted list of XRPC method NSIDs (queries + procedures) in a + * generated lexicon set — the same list that ends up in `.permissionSet`'s + * `lxm`. Use when the permission set isn't published yet and you need the + * scoped method list inline (e.g. in an OAuth client config). + * + * Filters `defs.main.type === "query" | "procedure"`. Sorted alphabetically + * so the output is stable across runs. */ +export function extractXrpcMethods(generated: Record): string[] { + const methods: string[] = []; + for (const [nsid, doc] of Object.entries(generated)) { + const mainType = (doc as { defs?: { main?: { type?: string } } })?.defs?.main?.type; + if (mainType === "query" || mainType === "procedure") methods.push(nsid); + } + return methods.sort(); +} + +/** Generate lexicons in-memory and return the sorted method NSID list. + * Convenience wrapper over `generateLexicons` + `extractXrpcMethods` for + * callers that don't care about the full lexicon map. */ +export function listXrpcMethods( + config: ContrailConfig, + options: { rootDir: string; lexiconDirs?: string[] } +): string[] { + const generated = generateLexicons({ + config, + rootDir: options.rootDir, + lexiconDirs: options.lexiconDirs, + quiet: true, + }); + return extractXrpcMethods(generated); +} + export interface GenerateOptions { config: ContrailConfig; /** Root project directory (for finding lexicon source files). */ diff --git a/tests/generate.test.ts b/tests/generate.test.ts index 8ab7a61..2a3456b 100644 --- a/tests/generate.test.ts +++ b/tests/generate.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll } from "vitest"; import { join } from "path"; -import { generateLexicons } from "../src/generate"; +import { generateLexicons, extractXrpcMethods, listXrpcMethods } from "../src/generate"; import type { ContrailConfig } from "../src/core/types"; const ROOT_DIR = join(__dirname, ".."); @@ -219,3 +219,39 @@ describe("search: no searchable field configured", () => { expect(params.search).toBeUndefined(); }); }); + +describe("extractXrpcMethods / listXrpcMethods", () => { + it("extracts only queries and procedures from a generated lexicon map", () => { + const lexicons = generate(BASIC_CONFIG); + const methods = extractXrpcMethods(lexicons); + // Sorted; includes admin + profile + listRecords/getRecord for the collection. + expect(methods).toEqual([...methods].sort()); + expect(methods).toContain("test.app.post.listRecords"); + expect(methods).toContain("test.app.post.getRecord"); + expect(methods).toContain("test.app.getProfile"); + // Does not include non-method defs (e.g. `defs`, records, permission-set). + expect(methods).not.toContain("test.app.permissionSet"); + }); + + it("listXrpcMethods matches the permissionSet lxm list for the same config", () => { + const methods = listXrpcMethods(BASIC_CONFIG, { rootDir: ROOT_DIR, lexiconDirs: [] }); + const lexicons = generate(BASIC_CONFIG); + const ps = (lexicons["test.app.permissionSet"] as any).defs.main.permissions[0]; + expect(ps.lxm).toEqual(methods); + }); + + it("includes realtime + community + spaces endpoints when those modules are enabled", () => { + const config: ContrailConfig = { + namespace: "test.comm", + collections: { message: { collection: "app.event.message" } }, + spaces: { type: "tools.atmo.event.space", serviceDid: "did:web:test.example#svc" }, + community: { masterKey: new Uint8Array(32).fill(1) }, + realtime: { ticketSecret: new Uint8Array(32).fill(2) }, + }; + const methods = listXrpcMethods(config, { rootDir: ROOT_DIR, lexiconDirs: [] }); + expect(methods).toContain("test.comm.space.createSpace"); + expect(methods).toContain("test.comm.community.adopt"); + expect(methods).toContain("test.comm.realtime.ticket"); + expect(methods).toContain("test.comm.realtime.subscribe"); + }); +}); -- 2.51.2 From a2a7287d9b55778fc23f3e54f2e43c19d809a34f Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:31:54 +0200 Subject: [PATCH 07/18] clone sveltekit example --- .../.claude/settings.local.json | 12 + examples/sveltekit-group-chat/.env.example | 6 + examples/sveltekit-group-chat/.gitignore | 26 + examples/sveltekit-group-chat/.npmrc | 1 + examples/sveltekit-group-chat/.prettierignore | 4 + examples/sveltekit-group-chat/.prettierrc | 15 + examples/sveltekit-group-chat/LICENSE | 21 + examples/sveltekit-group-chat/README.md | 77 + .../sveltekit-group-chat/eslint.config.js | 34 + .../statusphere/app/getCursor.json | 27 + .../statusphere/app/getOverview.json | 51 + .../statusphere/app/getProfile.json | 137 + .../statusphere/app/notifyOfUpdate.json | 59 + .../statusphere/app/permissionSet.json | 26 + .../statusphere/app/status/getRecord.json | 170 + .../statusphere/app/status/listRecords.json | 222 + .../lexicons-pulled/README.md | 5 + .../app/bsky/actor/profile.json | 67 + .../xyz/statusphere/status.json | 27 + examples/sveltekit-group-chat/package.json | 68 + examples/sveltekit-group-chat/pnpm-lock.yaml | 4750 +++++++++++ .../scripts/append-scheduled.ts | 29 + .../sveltekit-group-chat/scripts/generate.ts | 13 + examples/sveltekit-group-chat/scripts/sync.ts | 68 + examples/sveltekit-group-chat/src/app.css | 9 + examples/sveltekit-group-chat/src/app.d.ts | 34 + examples/sveltekit-group-chat/src/app.html | 12 + .../sveltekit-group-chat/src/hooks.server.ts | 15 + .../src/lib/atproto/auth.svelte.ts | 75 + .../src/lib/atproto/image-helper.ts | 152 + .../src/lib/atproto/index.ts | 20 + .../src/lib/atproto/methods.ts | 373 + .../src/lib/atproto/port.ts | 3 + .../src/lib/atproto/scripts/generate-key.ts | 4 + .../lib/atproto/scripts/generate-secret.ts | 3 + .../src/lib/atproto/scripts/setup-dev.ts | 60 + .../src/lib/atproto/scripts/tunnel.ts | 195 + .../src/lib/atproto/server/kv-store.ts | 38 + .../src/lib/atproto/server/oauth.remote.ts | 56 + .../src/lib/atproto/server/oauth.ts | 97 + .../src/lib/atproto/server/profile.ts | 51 + .../src/lib/atproto/server/repo.remote.ts | 94 + .../src/lib/atproto/server/session.ts | 73 + .../src/lib/atproto/server/signed-cookie.ts | 69 + .../src/lib/atproto/settings.ts | 30 + .../src/lib/atproto/ui/LoginModal.svelte | 22 + .../src/lib/contrail/client.ts | 32 + .../src/lib/contrail/config.ts | 14 + .../src/lib/contrail/index.ts | 31 + .../src/lib/emojis/icons.json | 7255 +++++++++++++++++ .../src/lib/emojis/index.ts | 23 + .../sveltekit-group-chat/src/lib/index.ts | 1 + .../oauth-client-metadata.json/+server.ts | 8 + .../routes/(oauth)/oauth/callback/+server.ts | 41 + .../routes/(oauth)/oauth/jwks.json/+server.ts | 8 + .../src/routes/+layout.server.ts | 25 + .../src/routes/+layout.svelte | 22 + .../src/routes/+layout.ts | 1 + .../src/routes/+page.server.ts | 42 + .../src/routes/+page.svelte | 188 + .../src/routes/api/cron/+server.ts | 15 + .../src/routes/xrpc/[...path]/+server.ts | 14 + .../sveltekit-group-chat/static/favicon.png | Bin 0 -> 1571 bytes examples/sveltekit-group-chat/static/og.png | Bin 0 -> 61667 bytes .../sveltekit-group-chat/svelte.config.js | 16 + examples/sveltekit-group-chat/tsconfig.json | 20 + examples/sveltekit-group-chat/vite.config.ts | 13 + examples/sveltekit-group-chat/wrangler.jsonc | 37 + 68 files changed, 15206 insertions(+) create mode 100644 examples/sveltekit-group-chat/.claude/settings.local.json create mode 100644 examples/sveltekit-group-chat/.env.example create mode 100644 examples/sveltekit-group-chat/.gitignore create mode 100644 examples/sveltekit-group-chat/.npmrc create mode 100644 examples/sveltekit-group-chat/.prettierignore create mode 100644 examples/sveltekit-group-chat/.prettierrc create mode 100644 examples/sveltekit-group-chat/LICENSE create mode 100644 examples/sveltekit-group-chat/README.md create mode 100644 examples/sveltekit-group-chat/eslint.config.js create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json create mode 100644 examples/sveltekit-group-chat/lexicons-pulled/README.md create mode 100644 examples/sveltekit-group-chat/lexicons-pulled/app/bsky/actor/profile.json create mode 100644 examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json create mode 100644 examples/sveltekit-group-chat/package.json create mode 100644 examples/sveltekit-group-chat/pnpm-lock.yaml create mode 100644 examples/sveltekit-group-chat/scripts/append-scheduled.ts create mode 100644 examples/sveltekit-group-chat/scripts/generate.ts create mode 100644 examples/sveltekit-group-chat/scripts/sync.ts create mode 100644 examples/sveltekit-group-chat/src/app.css create mode 100644 examples/sveltekit-group-chat/src/app.d.ts create mode 100644 examples/sveltekit-group-chat/src/app.html create mode 100644 examples/sveltekit-group-chat/src/hooks.server.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/auth.svelte.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/image-helper.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/index.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/methods.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/port.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-key.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-secret.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/scripts/setup-dev.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/scripts/tunnel.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/kv-store.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/oauth.remote.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/oauth.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/profile.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/session.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/settings.ts create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/ui/LoginModal.svelte create mode 100644 examples/sveltekit-group-chat/src/lib/contrail/client.ts create mode 100644 examples/sveltekit-group-chat/src/lib/contrail/config.ts create mode 100644 examples/sveltekit-group-chat/src/lib/contrail/index.ts create mode 100644 examples/sveltekit-group-chat/src/lib/emojis/icons.json create mode 100644 examples/sveltekit-group-chat/src/lib/emojis/index.ts create mode 100644 examples/sveltekit-group-chat/src/lib/index.ts create mode 100644 examples/sveltekit-group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/(oauth)/oauth/callback/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/+layout.server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/+layout.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/+layout.ts create mode 100644 examples/sveltekit-group-chat/src/routes/+page.server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/+page.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/api/cron/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts create mode 100644 examples/sveltekit-group-chat/static/favicon.png create mode 100644 examples/sveltekit-group-chat/static/og.png create mode 100644 examples/sveltekit-group-chat/svelte.config.js create mode 100644 examples/sveltekit-group-chat/tsconfig.json create mode 100644 examples/sveltekit-group-chat/vite.config.ts create mode 100644 examples/sveltekit-group-chat/wrangler.jsonc diff --git a/examples/sveltekit-group-chat/.claude/settings.local.json b/examples/sveltekit-group-chat/.claude/settings.local.json new file mode 100644 index 0000000..32f6420 --- /dev/null +++ b/examples/sveltekit-group-chat/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(npx tsc:*)", + "mcp__plugin_svelte_svelte__svelte-autofixer", + "mcp__plugin_svelte_svelte__get-documentation", + "Bash(pnpm check:*)", + "Bash(pnpm env:generate-secret:*)", + "Bash(pnpm build:agent-setup:*)" + ] + } +} diff --git a/examples/sveltekit-group-chat/.env.example b/examples/sveltekit-group-chat/.env.example new file mode 100644 index 0000000..ede6fba --- /dev/null +++ b/examples/sveltekit-group-chat/.env.example @@ -0,0 +1,6 @@ +# Generate both with: pnpm env:setup-dev +CLIENT_ASSERTION_KEY= +COOKIE_SECRET= + +# Set to your tunnel URL to use a confidential client in dev +# OAUTH_PUBLIC_URL=https://your-tunnel.trycloudflare.com diff --git a/examples/sveltekit-group-chat/.gitignore b/examples/sveltekit-group-chat/.gitignore new file mode 100644 index 0000000..1c43954 --- /dev/null +++ b/examples/sveltekit-group-chat/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Generated types (regenerated via pnpm generate:pull) +src/lexicon-types/ diff --git a/examples/sveltekit-group-chat/.npmrc b/examples/sveltekit-group-chat/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/examples/sveltekit-group-chat/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/examples/sveltekit-group-chat/.prettierignore b/examples/sveltekit-group-chat/.prettierignore new file mode 100644 index 0000000..ab78a95 --- /dev/null +++ b/examples/sveltekit-group-chat/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/examples/sveltekit-group-chat/.prettierrc b/examples/sveltekit-group-chat/.prettierrc new file mode 100644 index 0000000..7ebb855 --- /dev/null +++ b/examples/sveltekit-group-chat/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/examples/sveltekit-group-chat/LICENSE b/examples/sveltekit-group-chat/LICENSE new file mode 100644 index 0000000..696e4f6 --- /dev/null +++ b/examples/sveltekit-group-chat/LICENSE @@ -0,0 +1,21 @@ +MIT License Copyright (c) 2025 flo-bit + +Permission is hereby granted, free of +charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice +(including the next paragraph) shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/examples/sveltekit-group-chat/README.md b/examples/sveltekit-group-chat/README.md new file mode 100644 index 0000000..4917318 --- /dev/null +++ b/examples/sveltekit-group-chat/README.md @@ -0,0 +1,77 @@ +# sveltekit + contrail + atproto OAuth + +A sveltekit example app on cloudflare workers with self-hosted atproto record indexing via [contrail](https://github.com/flo-bit/contrail), fully typed queries, and OAuth authentication. + +## Setup + +```sh +pnpm install +pnpm dev +``` + +Dev mode uses a loopback OAuth client — no keys or Cloudflare setup needed. + +## Config + +Define which AT Protocol collections to index in `src/lib/contrail/config.ts`: + +```ts +import type { ContrailConfig } from '@atmo-dev/contrail'; + +export const config: ContrailConfig = { + namespace: 'statusphere.app', + collections: { + status: { // short name → URL path segment + collection: 'xyz.statusphere.status', // full NSID of the record type + queryable: { + status: {}, // equality filter (?status=...) + createdAt: { type: 'range' } // range filter (?createdAtMin=...&createdAtMax=...) + } + } + } +}; +``` + +After changing the config, run `pnpm generate:pull` to regenerate lexicons and types. + +Run `pnpm sync` to backfill existing records from the network. + +Wrangler bindings (`wrangler.jsonc`): + +- **D1** (`DB`) — contrail's database +- **KV** (`OAUTH_SESSIONS`, `OAUTH_STATES`) — OAuth session storage +- **Vars** — `CRON_SECRET`, `OAUTH_PUBLIC_URL`, `CLIENT_ASSERTION_KEY`, `COOKIE_SECRET` + +## Deploy + +```sh +npx wrangler d1 create statusphere +# Add database_id to wrangler.jsonc + +pnpm build +npx wrangler deploy +``` + +## How it works + +**Contrail** indexes AT Protocol records into D1 via Jetstream (cron, every minute). When a user posts, `contrail.notify()` indexes it immediately. + +**Typed queries** use `@atcute/client` with an in-process handler — full type safety, zero HTTP overhead: + +```ts +const client = getClient(platform!.env.DB); +const res = await client.get('statusphere.app.status.listRecords', { + params: { limit: 50, profiles: true } // typed params +}); +res.data.records // typed response +``` + +Types are generated from contrail's config via `pnpm generate:pull`, which produces lexicon JSON and TypeScript types that register with `@atcute/client`. + +**Scheduled ingestion** works around SvelteKit's lack of `scheduled` export support ([sveltejs/kit#4841](https://github.com/sveltejs/kit/issues/4841)) by appending a handler post-build that self-calls `/api/cron`. + +**OAuth** uses `@atcute/oauth-node-client` with KV-backed sessions and HMAC-signed cookies. + +## License + +MIT diff --git a/examples/sveltekit-group-chat/eslint.config.js b/examples/sveltekit-group-chat/eslint.config.js new file mode 100644 index 0000000..aa5987f --- /dev/null +++ b/examples/sveltekit-group-chat/eslint.config.js @@ -0,0 +1,34 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte'], + + languageOptions: { + parserOptions: { + parser: ts.parser + } + } + } +); diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json new file mode 100644 index 0000000..f85bdbf --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "statusphere.app.getCursor", + "defs": { + "main": { + "type": "query", + "description": "Get the current cursor position", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "time_us": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "seconds_ago": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json new file mode 100644 index 0000000..67dd2f5 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "statusphere.app.getOverview", + "defs": { + "main": { + "type": "query", + "description": "Get an overview of all indexed collections", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "total_records", + "collections" + ], + "properties": { + "total_records": { + "type": "integer" + }, + "collections": { + "type": "array", + "items": { + "type": "ref", + "ref": "#collectionStats" + } + } + } + } + } + }, + "collectionStats": { + "type": "object", + "required": [ + "collection", + "records", + "unique_users" + ], + "properties": { + "collection": { + "type": "string" + }, + "records": { + "type": "integer" + }, + "unique_users": { + "type": "integer" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json new file mode 100644 index 0000000..4706150 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json @@ -0,0 +1,137 @@ +{ + "lexicon": 1, + "id": "statusphere.app.getProfile", + "defs": { + "main": { + "type": "query", + "description": "Get a user's profiles by DID or handle", + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "DID or handle of the user" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "profiles" + ], + "properties": { + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json new file mode 100644 index 0000000..f34ded3 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json @@ -0,0 +1,59 @@ +{ + "lexicon": 1, + "id": "statusphere.app.notifyOfUpdate", + "defs": { + "main": { + "type": "procedure", + "description": "Notify of a record change for immediate indexing. Fetches the record from the user's PDS and indexes (or deletes) it.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "Single AT URI to fetch and index" + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 25, + "description": "Batch of AT URIs to fetch and index (max 25)" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "indexed", + "deleted" + ], + "properties": { + "indexed": { + "type": "integer", + "description": "Number of records created or updated" + }, + "deleted": { + "type": "integer", + "description": "Number of records deleted (not found on PDS)" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Errors for individual URIs that could not be processed" + } + } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json new file mode 100644 index 0000000..232cfee --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "statusphere.app.permissionSet", + "defs": { + "main": { + "type": "permission-set", + "title": "statusphere.app", + "description": "All XRPC methods exposed by the statusphere.app service.", + "permissions": [ + { + "type": "permission", + "resource": "rpc", + "inheritAud": true, + "lxm": [ + "statusphere.app.getCursor", + "statusphere.app.getOverview", + "statusphere.app.getProfile", + "statusphere.app.notifyOfUpdate", + "statusphere.app.status.getRecord", + "statusphere.app.status.listRecords" + ] + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json new file mode 100644 index 0000000..da31528 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json @@ -0,0 +1,170 @@ +{ + "lexicon": 1, + "id": "statusphere.app.status.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single xyz.statusphere.status record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "xyz.statusphere.status#main" + }, + "time_us": { + "type": "integer" + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json new file mode 100644 index 0000000..8fb9309 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json @@ -0,0 +1,222 @@ +{ + "lexicon": 1, + "id": "statusphere.app.status.listRecords", + "defs": { + "main": { + "type": "query", + "description": "Query xyz.statusphere.status records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "status": { + "type": "string", + "description": "Filter by status" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "sort": { + "type": "string", + "knownValues": [ + "status", + "createdAt" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "xyz.statusphere.status#main" + }, + "time_us": { + "type": "integer" + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-pulled/README.md b/examples/sveltekit-group-chat/lexicons-pulled/README.md new file mode 100644 index 0000000..812df07 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-pulled/README.md @@ -0,0 +1,5 @@ +# lexicon sources + +this directory contains lexicon documents pulled from the following sources: + +- atproto (nsids: app.bsky.actor.profile, xyz.statusphere.status) diff --git a/examples/sveltekit-group-chat/lexicons-pulled/app/bsky/actor/profile.json b/examples/sveltekit-group-chat/lexicons-pulled/app/bsky/actor/profile.json new file mode 100644 index 0000000..1d22cc4 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-pulled/app/bsky/actor/profile.json @@ -0,0 +1,67 @@ +{ + "id": "app.bsky.actor.profile", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": ["com.atproto.label.defs#selfLabels"], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "description": "A declaration of a Bluesky account profile." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json b/examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json new file mode 100644 index 0000000..c5a314e --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json @@ -0,0 +1,27 @@ +{ + "id": "xyz.statusphere.status", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": ["status", "createdAt"], + "properties": { + "status": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "maxGraphemes": 1 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/examples/sveltekit-group-chat/package.json b/examples/sveltekit-group-chat/package.json new file mode 100644 index 0000000..f45b5ab --- /dev/null +++ b/examples/sveltekit-group-chat/package.json @@ -0,0 +1,68 @@ +{ + "name": "svelte-cloudflare-statusphere", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build && tsx scripts/append-scheduled.ts", + "generate": "tsx scripts/generate.ts", + "generate:pull": "tsx scripts/generate.ts && lex-cli pull && tsx scripts/generate.ts && lex-cli pull && lex-cli generate", + "sync": "tsx scripts/sync.ts", + "sync:remote": "tsx scripts/sync.ts --remote", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", + "env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", + "env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts", + "tunnel": "npx tsx src/lib/atproto/scripts/tunnel.ts" + }, + "devDependencies": { + "@atcute/atproto": "^3.1.10", + "@atcute/bluesky": "^3.3.0", + "@atcute/client": "^4.2.1", + "@atcute/identity-resolver": "^1.2.2", + "@atcute/lex-cli": "^2.5.3", + "@atcute/lexicon-doc": "^2.1.2", + "@atcute/lexicons": "^1.2.9", + "@atcute/oauth-node-client": "^1.1.0", + "@atcute/tid": "^1.1.2", + "@cloudflare/workers-types": "^4.20260317.1", + "@eslint/compat": "^2.0.3", + "@eslint/js": "^10.0.1", + "@sveltejs/adapter-cloudflare": "^7.2.8", + "@sveltejs/kit": "^2.55.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/vite": "^4.2.2", + "bits-ui": "^2.16.4", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.16.0", + "globals": "^17.4.0", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.5.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.55.0", + "svelte-check": "^4.4.5", + "tailwindcss": "^4.2.2", + "tsx": "^4.21.0", + "typescript": "^6.0.2", + "typescript-eslint": "^8.57.2", + "vite": "^8.0.3", + "wrangler": "^4.78.0" + }, + "license": "MIT", + "dependencies": { + "@atcute/jetstream": "^1.1.2", + "@atmo-dev/contrail": "^0.1.1", + "@foxui/core": "^0.9.1", + "@foxui/social": "^0.8.10", + "@foxui/time": "^0.8.5", + "valibot": "^1.3.1" + } +} diff --git a/examples/sveltekit-group-chat/pnpm-lock.yaml b/examples/sveltekit-group-chat/pnpm-lock.yaml new file mode 100644 index 0000000..35f6a60 --- /dev/null +++ b/examples/sveltekit-group-chat/pnpm-lock.yaml @@ -0,0 +1,4750 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@atcute/jetstream': + specifier: ^1.1.2 + version: 1.1.2 + '@foxui/core': + specifier: ^0.8.2 + version: 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) + '@foxui/social': + specifier: ^0.8.4 + version: 0.8.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2) + '@foxui/time': + specifier: ^0.8.2 + version: 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) + contrail: + specifier: github:flo-bit/contrail + version: https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf(@atcute/identity@1.1.4) + valibot: + specifier: ^1.3.1 + version: 1.3.1(typescript@6.0.2) + devDependencies: + '@atcute/atproto': + specifier: ^3.1.10 + version: 3.1.10 + '@atcute/bluesky': + specifier: ^3.3.0 + version: 3.3.0 + '@atcute/client': + specifier: ^4.2.1 + version: 4.2.1 + '@atcute/identity-resolver': + specifier: ^1.2.2 + version: 1.2.2(@atcute/identity@1.1.4) + '@atcute/lex-cli': + specifier: ^2.5.3 + version: 2.5.3 + '@atcute/lexicon-doc': + specifier: ^2.1.2 + version: 2.1.2 + '@atcute/lexicons': + specifier: ^1.2.9 + version: 1.2.9 + '@atcute/oauth-node-client': + specifier: ^1.1.0 + version: 1.1.0 + '@atcute/tid': + specifier: ^1.1.2 + version: 1.1.2 + '@cloudflare/workers-types': + specifier: ^4.20260317.1 + version: 4.20260317.1 + '@eslint/compat': + specifier: ^2.0.3 + version: 2.0.3(eslint@10.1.0(jiti@2.6.1)) + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) + '@sveltejs/adapter-cloudflare': + specifier: ^7.2.8 + version: 7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.78.0(@cloudflare/workers-types@4.20260317.1)) + '@sveltejs/kit': + specifier: ^2.55.0 + version: 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@4.2.2) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + bits-ui: + specifier: ^2.16.4 + version: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + eslint: + specifier: ^10.1.0 + version: 10.1.0(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-svelte: + specifier: ^3.16.0 + version: 3.16.0(eslint@10.1.0(jiti@2.6.1))(svelte@5.55.0) + globals: + specifier: ^17.4.0 + version: 17.4.0 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + prettier-plugin-svelte: + specifier: ^3.5.1 + version: 3.5.1(prettier@3.8.1)(svelte@5.55.0) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.0))(prettier@3.8.1) + svelte: + specifier: ^5.55.0 + version: 5.55.0 + svelte-check: + specifier: ^4.4.5 + version: 4.4.5(picomatch@4.0.4)(svelte@5.55.0)(typescript@6.0.2) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + typescript-eslint: + specifier: ^8.57.2 + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + vite: + specifier: ^8.0.3 + version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + wrangler: + specifier: ^4.78.0 + version: 4.78.0(@cloudflare/workers-types@4.20260317.1) + +packages: + + '@atcute/atproto@3.1.10': + resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==, tarball: https://registry.npmjs.org/@atcute/atproto/-/atproto-3.1.10.tgz} + + '@atcute/bluesky-richtext-segmenter@3.0.0': + resolution: {integrity: sha512-NhZTUKtFpeBBbILwAcxj5u4RobIoHOmGw3CAaaEFNebKYSvmTecrXJ7XufHw5DFOUdr8SiKXQVRQxGAxulMNWg==, tarball: https://registry.npmjs.org/@atcute/bluesky-richtext-segmenter/-/bluesky-richtext-segmenter-3.0.0.tgz} + + '@atcute/bluesky@3.3.0': + resolution: {integrity: sha512-TrLnlxuD6F/D2ZYzJ3aCiRD0yiFuhmVsd6oULNzzr8V9Xzlufg0yxkRiGmbMiF2iI508y/MFi6vzo625301c5A==, tarball: https://registry.npmjs.org/@atcute/bluesky/-/bluesky-3.3.0.tgz} + + '@atcute/car@5.1.1': + resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==, tarball: https://registry.npmjs.org/@atcute/car/-/car-5.1.1.tgz} + + '@atcute/cbor@2.3.2': + resolution: {integrity: sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ==, tarball: https://registry.npmjs.org/@atcute/cbor/-/cbor-2.3.2.tgz} + + '@atcute/cid@2.4.1': + resolution: {integrity: sha512-bwhna69RCv7yetXudtj+2qrMPYvhhIQqvJz6YUpUS98v7OdF3X2dnye9Nig2NDrklZcuyOsu7sQo7GOykJXRLQ==, tarball: https://registry.npmjs.org/@atcute/cid/-/cid-2.4.1.tgz} + + '@atcute/client@4.2.1': + resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==, tarball: https://registry.npmjs.org/@atcute/client/-/client-4.2.1.tgz} + + '@atcute/crypto@2.4.1': + resolution: {integrity: sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==, tarball: https://registry.npmjs.org/@atcute/crypto/-/crypto-2.4.1.tgz} + + '@atcute/identity-resolver@1.2.2': + resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==, tarball: https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.2.2.tgz} + peerDependencies: + '@atcute/identity': ^1.0.0 + + '@atcute/identity@1.1.4': + resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==, tarball: https://registry.npmjs.org/@atcute/identity/-/identity-1.1.4.tgz} + + '@atcute/jetstream@1.1.2': + resolution: {integrity: sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==, tarball: https://registry.npmjs.org/@atcute/jetstream/-/jetstream-1.1.2.tgz} + + '@atcute/lex-cli@2.5.3': + resolution: {integrity: sha512-829rvezMOfRkJQRKvupNT8TWT/YYffJ2QsB80D9aPjkXSogrETZA7xZcPaMZBXg+mJaVbLO9S4ThPQmlF0L4UQ==, tarball: https://registry.npmjs.org/@atcute/lex-cli/-/lex-cli-2.5.3.tgz} + hasBin: true + + '@atcute/lexicon-doc@2.1.2': + resolution: {integrity: sha512-jTLcOka7b8BIn2SnIZm2m7l6unlJ0gpgW1MnRpSqNbly/AvyRUR/GREduh/QmjT4SGasDm8vdhrM0kOSPFpDLQ==, tarball: https://registry.npmjs.org/@atcute/lexicon-doc/-/lexicon-doc-2.1.2.tgz} + + '@atcute/lexicon-resolver@0.1.6': + resolution: {integrity: sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==, tarball: https://registry.npmjs.org/@atcute/lexicon-resolver/-/lexicon-resolver-0.1.6.tgz} + peerDependencies: + '@atcute/identity': ^1.1.0 + '@atcute/identity-resolver': ^1.1.3 + + '@atcute/lexicons@1.2.9': + resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==, tarball: https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.9.tgz} + + '@atcute/mst@1.0.0': + resolution: {integrity: sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==, tarball: https://registry.npmjs.org/@atcute/mst/-/mst-1.0.0.tgz} + + '@atcute/multibase@1.2.0': + resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==, tarball: https://registry.npmjs.org/@atcute/multibase/-/multibase-1.2.0.tgz} + + '@atcute/oauth-crypto@0.1.0': + resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==, tarball: https://registry.npmjs.org/@atcute/oauth-crypto/-/oauth-crypto-0.1.0.tgz} + + '@atcute/oauth-keyset@0.1.0': + resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==, tarball: https://registry.npmjs.org/@atcute/oauth-keyset/-/oauth-keyset-0.1.0.tgz} + + '@atcute/oauth-node-client@1.1.0': + resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==, tarball: https://registry.npmjs.org/@atcute/oauth-node-client/-/oauth-node-client-1.1.0.tgz} + + '@atcute/oauth-types@0.1.1': + resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==, tarball: https://registry.npmjs.org/@atcute/oauth-types/-/oauth-types-0.1.1.tgz} + + '@atcute/repo@0.1.3': + resolution: {integrity: sha512-kN4gkrkQgJwI5xkVQ9zSdI8ULR47uH48EdJdjMvKAWsSDu8zKYd8ZQ6n778qa6o3WYEq02v9QVf7pzeU+3fYVA==, tarball: https://registry.npmjs.org/@atcute/repo/-/repo-0.1.3.tgz} + + '@atcute/tid@1.1.2': + resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==, tarball: https://registry.npmjs.org/@atcute/tid/-/tid-1.1.2.tgz} + + '@atcute/time-ms@1.3.2': + resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==, tarball: https://registry.npmjs.org/@atcute/time-ms/-/time-ms-1.3.2.tgz} + + '@atcute/uint8array@1.1.1': + resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==, tarball: https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.1.1.tgz} + + '@atcute/util-fetch@1.0.5': + resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==, tarball: https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.5.tgz} + + '@atcute/util-text@1.2.0': + resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==, tarball: https://registry.npmjs.org/@atcute/util-text/-/util-text-1.2.0.tgz} + + '@atcute/varint@2.0.0': + resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==, tarball: https://registry.npmjs.org/@atcute/varint/-/varint-2.0.0.tgz} + + '@badrap/valita@0.4.6': + resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==, tarball: https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz} + engines: {node: '>= 18'} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==, tarball: https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.16.0': + resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==, tarball: https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260317.1': + resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260317.1': + resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260317.1': + resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260317.1': + resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260317.1': + resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, tarball: https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz} + engines: {node: '>=12'} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==, tarball: https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==, tarball: https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, tarball: https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, tarball: https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@2.0.3': + resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==, tarball: https://registry.npmjs.org/@eslint/compat/-/compat-2.0.3.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.23.3': + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==, tarball: https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==, tarball: https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==, tarball: https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==, tarball: https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.3': + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==, tarball: https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==, tarball: https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==, tarball: https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz} + + '@foxui/core@0.8.2': + resolution: {integrity: sha512-cVfFCQX4SfL7mezfQHAHln8M+ZiEZlwa4lIXMFUATLuhl4YCcwv7IvvtU+hMvq2ky+vsCEEry1XvXOLdSe6zAA==, tarball: https://registry.npmjs.org/@foxui/core/-/core-0.8.2.tgz} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/social@0.8.4': + resolution: {integrity: sha512-+5pV402gpEwxSdWb0EMr2SKG/kutCEXzQI930oGA6Q8O/Dnk14noCf/hYAtZvBlAd0cwa6Grlgt7/VEwzU5NDg==, tarball: https://registry.npmjs.org/@foxui/social/-/social-0.8.4.tgz} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/text@0.8.2': + resolution: {integrity: sha512-f1WotRAzZszqcg/c0RBs9Wuy7KdyHhsZRJNBaufFLl+hzMhCSJQq11lQPdRCUITKP2rBfeguZfqkAcqi0PuI8w==, tarball: https://registry.npmjs.org/@foxui/text/-/text-0.8.2.tgz} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/time@0.8.2': + resolution: {integrity: sha512-w60jJij44bcle905AL/vBijyI/ilY4cSA21yxYg8C6swm5uH5wooW1fSFaTuC/Lg0GfhwTkW963kz66vVsYbiw==, tarball: https://registry.npmjs.org/@foxui/time/-/time-0.8.2.tgz} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, tarball: https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, tarball: https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, tarball: https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, tarball: https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==, tarball: https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, tarball: https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, tarball: https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, tarball: https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, tarball: https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, tarball: https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, tarball: https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, tarball: https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, tarball: https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, tarball: https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, tarball: https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, tarball: https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, tarball: https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, tarball: https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, tarball: https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, tarball: https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, tarball: https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@internationalized/date@3.10.1': + resolution: {integrity: sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==, tarball: https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, tarball: https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz} + + '@mary-ext/event-iterator@1.0.0': + resolution: {integrity: sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==, tarball: https://registry.npmjs.org/@mary-ext/event-iterator/-/event-iterator-1.0.0.tgz} + + '@mary-ext/simple-event-emitter@1.0.1': + resolution: {integrity: sha512-9+VvZisxZ/gSg+JJH7hmXaA8Qj42Qjz3O58RSB+INYc8iLA0icATZxHB9vKbj59ojDGZjO3hCKzMXocx3L0H8w==, tarball: https://registry.npmjs.org/@mary-ext/simple-event-emitter/-/simple-event-emitter-1.0.1.tgz} + + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==, tarball: https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/secp256k1@3.0.0': + resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==, tarball: https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz} + + '@number-flow/svelte@0.4.0': + resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==, tarball: https://registry.npmjs.org/@number-flow/svelte/-/svelte-0.4.0.tgz} + peerDependencies: + svelte: ^4 || ^5 + + '@optique/core@0.6.11': + resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==, tarball: https://registry.npmjs.org/@optique/core/-/core-0.6.11.tgz} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + + '@optique/run@0.6.11': + resolution: {integrity: sha512-tsXBEygGSzNpFK2gjsRlXBn7FiScUeLFWIZNpoAZ8iG85Km0/3K9xgqlQAXoQ+uEZBe4XplnzyCDvmEgbyNT8w==, tarball: https://registry.npmjs.org/@optique/run/-/run-0.6.11.tgz} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==, tarball: https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, tarball: https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==, tarball: https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==, tarball: https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==, tarball: https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz} + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==, tarball: https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==, tarball: https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==, tarball: https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==, tarball: https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==, tarball: https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==, tarball: https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==, tarball: https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==, tarball: https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz} + + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==, tarball: https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-cloudflare@7.2.8': + resolution: {integrity: sha512-bIdhY/Fi4AQmqiBdQVKnafH1h9Gw+xbCvHyUu4EouC8rJOU02zwhi14k/FDhQ0mJF1iblIu3m8UNQ8GpGIvIOQ==, tarball: https://registry.npmjs.org/@sveltejs/adapter-cloudflare/-/adapter-cloudflare-7.2.8.tgz} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + wrangler: ^4.0.0 + + '@sveltejs/kit@2.55.0': + resolution: {integrity: sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==, tarball: https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte@7.0.0': + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==, tarball: https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@swc/helpers@0.5.20': + resolution: {integrity: sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==, tarball: https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz} + + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==, tarball: https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==, tarball: https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==, tarball: https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tiptap/core@3.21.0': + resolution: {integrity: sha512-IfnQiuEeabDSPr1C/zHFTbnvlTf5z0DE/d/xz4C6bkL4ZBDJ3rr99h2qsaV0l8F+kbNswZMlQdM8rxNlMy95fQ==, tarball: https://registry.npmjs.org/@tiptap/core/-/core-3.21.0.tgz} + peerDependencies: + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-blockquote@3.21.0': + resolution: {integrity: sha512-JDM/RR6rM0dMCZ1UnEf7eqmN6pAdIa2llhN+E24HdTGNJCklMFhLAGE/OT8/1r7M0WWA9GVO7/PTe4EdGh6+lQ==, tarball: https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-bold@3.21.0': + resolution: {integrity: sha512-iyEJRzG7XTCPlHwEDzUw3HnuYYCfL7lNpcCHmxcpYMrIUA8rv7EUxerIwApT6xY8hQ/07ljuJKgOyPvnJOOzuA==, tarball: https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-bubble-menu@3.21.0': + resolution: {integrity: sha512-/fabRRhhf8i4LAx9e8xz9ppqN5KgdJk3TxMuxAD5vAWGsejvhSoPa8O8H/QwwyntXm1Vue8aQiMHsUk48b2hGQ==, tarball: https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-bullet-list@3.21.0': + resolution: {integrity: sha512-PWNF+xwxgOeXYGD88sCQLKL0eBoQqjUnZNALxBjN3Y7x4llalh42rHOp2Nt2t6UbQgqTBtBzU/uFcussTpxreQ==, tarball: https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.21.0.tgz} + peerDependencies: + '@tiptap/extension-list': ^3.21.0 + + '@tiptap/extension-code-block-lowlight@3.21.0': + resolution: {integrity: sha512-79sS0tqoGVX6wq30ejzohpUVLeGOkTTUn5hCqjsniyYPTEtrn4tHyBnS4Du2TbrDV4SqcwgisWExhuB8pfEdYQ==, tarball: https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/extension-code-block': ^3.21.0 + '@tiptap/pm': ^3.21.0 + highlight.js: ^11 + lowlight: ^2 || ^3 + + '@tiptap/extension-code-block@3.21.0': + resolution: {integrity: sha512-zrVOcOzDCjHQ8NJcC+qHmZZKiwnP/NMSb3qVJlSMN8TzuHept1MZCDa2Mbo70O6I0txo456SGuXB9sqV1vHmGg==, tarball: https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-code@3.21.0': + resolution: {integrity: sha512-D7wA9jp+4X2r1f3FIoga73s6Rn4rmZY57Jes6a4rK3HY+3yHk1r057pPIZSY8Drfs97jxHQVFdfUYUomLSFYBA==, tarball: https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-document@3.21.0': + resolution: {integrity: sha512-7oCyzXI9ChvJQUlr23AURdfVar4OIsrYUvqdhEwo3bjcI/Q/j0KJiXfuh6ZzL5eVaINSailH53sZaGg4THQtUg==, tarball: https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-dropcursor@3.21.0': + resolution: {integrity: sha512-6fsDSVAM2iz7eElvT6iivMrGBGjIP/oPigVZ/SPm6f31phaYhz6TIOEgV/Lr2jaPIOgyK4U0cU4Yd4KUBCmhzQ==, tarball: https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.21.0.tgz} + peerDependencies: + '@tiptap/extensions': ^3.21.0 + + '@tiptap/extension-floating-menu@3.21.0': + resolution: {integrity: sha512-n2HzTB+I/5rAl8R/1sKMv92JiY1oDK1hroXizxEKYa6dskJcAMW0CfYyPcPOZWQQEe7qoeOvQISr2ooLAKW+Mw==, tarball: https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.21.0.tgz} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-gapcursor@3.21.0': + resolution: {integrity: sha512-wGjgAoYBTvPAe9QYMI5px355XcNeMkaUrMY9IHbMqgqdmHcDxqooxM4H6sYVX2CRcHwXy4I8NQAoOhSYrQJDMg==, tarball: https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.21.0.tgz} + peerDependencies: + '@tiptap/extensions': ^3.21.0 + + '@tiptap/extension-hard-break@3.21.0': + resolution: {integrity: sha512-6JFVSAOQ1qhQHi9mVcdn2/XO8YIMgYV8zjarzNUzP6Sf2waeE5BLXjlg6rIH/945sY1J+FndTojLru6gQ07a5A==, tarball: https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-heading@3.21.0': + resolution: {integrity: sha512-ji6VJmoRnDzAHYflEYEZohMHRi77UGLW1o3ua7UhI32iJ9nuYssbPNuzEeE4SvENMQwZRszad5+a+dKAa+NC7g==, tarball: https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-history@3.21.0': + resolution: {integrity: sha512-aYUxm0fLXGfn4XAsRq53lOX1cB+GRF8rScVuDHmQfEaJHNo09DSRtvgWnDHVdqIS8FvKkBcTO8eyPOBPMvkWiA==, tarball: https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-3.21.0.tgz} + peerDependencies: + '@tiptap/extensions': ^3.21.0 + + '@tiptap/extension-horizontal-rule@3.21.0': + resolution: {integrity: sha512-vNBnOfFEY62CoJPGo4nonRM7RiOvhII1vhoO+WFr1GxDqCAfmEFjToflt7JT1UJdo6lMVcD+aaaAgOiuSz5p6g==, tarball: https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-image@3.21.0': + resolution: {integrity: sha512-W9786a2K4LSZJMPeRLmoDulJeXOsM0ueRV2MHjTol7ikPRauROB7GUbAz9DyPAJHA2AGUfpswnGAYPO3tz5CLg==, tarball: https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-italic@3.21.0': + resolution: {integrity: sha512-2I8oPvwyXhRn1k8lbDFIutzvhtLEjoO5mmQCNX4TnT4PdxxaSrK9+ihYg12VeqhUeO7dg1MKiFqws0HVBrwzWg==, tarball: https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-link@3.21.0': + resolution: {integrity: sha512-oMU7Yve1sbgBsaFAUc2R0GPf4d3ZPVJeMUFC6b6X9rJIvx/IhEUEn9toQcSBGfp02uWK9NdQyIFYFdWlVXH++w==, tarball: https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-list-item@3.21.0': + resolution: {integrity: sha512-1ZymZmlQVbAoC4q5x3cro0v5+3I6l+BHqbhIMQLjQFlAOJfcE0pvqRzAFW7PduxUj41tXEtsYqp2NREvO9F5Fg==, tarball: https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.21.0.tgz} + peerDependencies: + '@tiptap/extension-list': ^3.21.0 + + '@tiptap/extension-list-keymap@3.21.0': + resolution: {integrity: sha512-EzrfW3ASNFPWKhR8sNOq7Kqw4hvaTAOn4dlI7chB8HIANSrlyPOUn+eKAnO6HQgsUgsbjg2GbTUrGrxcoLykUg==, tarball: https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.21.0.tgz} + peerDependencies: + '@tiptap/extension-list': ^3.21.0 + + '@tiptap/extension-list@3.21.0': + resolution: {integrity: sha512-KeBlEtLrGce2d3dgL89hmwWEtREuzlW4XY5bYWpKNvCbFqvdSb3n7vkdkw32YclZmMWxAcABgW6ucCStkE0rsQ==, tarball: https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/extension-ordered-list@3.21.0': + resolution: {integrity: sha512-+d+0orokMfqaBfvr9tUBgGvo2ZCV+fR3JzsJTmnLBWOkhBSJN7H4pnfXPTue0qwspUwRmkLJxdIlU+J7HkMrng==, tarball: https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.21.0.tgz} + peerDependencies: + '@tiptap/extension-list': ^3.21.0 + + '@tiptap/extension-paragraph@3.21.0': + resolution: {integrity: sha512-cMPG/jCoZ9NmLZ5ctFziILaxJGfDtMTb5OLBhifMFZeMVwF1pEJIygDEfnX/HSruv507weZSQG4pERO2tRszMg==, tarball: https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-placeholder@3.21.0': + resolution: {integrity: sha512-fs+cQqMh1d1naV6OgOhP/0qbRJwtw8DpQMj3/oqGKbaRRKIeecEaZPXYRd7MYa4e9K0Cfk5Bm0MNs9lwu/BYsw==, tarball: https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.21.0.tgz} + peerDependencies: + '@tiptap/extensions': ^3.21.0 + + '@tiptap/extension-strike@3.21.0': + resolution: {integrity: sha512-easnVaN11Wl+5fOtfvzJ10J762S9TRXZaMj5rLBGavgf82DCYHqhGhBqpLQrJ41r4nPABGlYvTRoxfvBLB74Lg==, tarball: https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-text@3.21.0': + resolution: {integrity: sha512-Zx8QdB8a5iBuE4uO21c3BjmpBfaJEr2Jd1QFnsdgx11fm6P7dGgZaGko1FaINhfOPRGTN6O/kiF02cDMdOHa/w==, tarball: https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-typography@3.21.0': + resolution: {integrity: sha512-/nHtkQTtBPmQPe4JnEGBxxCKIv2FToSB3PUfh7b4cccNkbs7yJZO30ySNJcMzNfn8pfSQPkJxZJcFuSm9RjZdg==, tarball: https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extension-underline@3.21.0': + resolution: {integrity: sha512-gGmBEymbWnr8AIS8bI/bPw5rcwo7wAFcBw/TsLd1nAanu1dDqSRNDBrit3m02Ru+D88u2SfNvmbOPI1pz+1f5w==, tarball: https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + + '@tiptap/extensions@3.21.0': + resolution: {integrity: sha512-MN1uh5PmHT1F2BNsbc21MIS0AMFFA73oODlp/4ckpBR4o5AxRwV+8f43Cd52UL4MgMkKj/A+QfZ7iK9IDb0h5A==, tarball: https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/markdown@3.21.0': + resolution: {integrity: sha512-VhnHhB9LeEfB2xnbNZUV2itK0ae4iERbr+a4/ZrlZB9auPtggW4fIob/bQGOaeU+GZtk1xDLyKUDyKEEwgTXMw==, tarball: https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tiptap/pm@3.21.0': + resolution: {integrity: sha512-I3sNo7oMMsR6FFz1ecvPb9uCF0VQuS2WV67j8Io2M7DJicRWCE/GM5DaiYjTeWBbnByk6BuG0txoJATAqPVliQ==, tarball: https://registry.npmjs.org/@tiptap/pm/-/pm-3.21.0.tgz} + + '@tiptap/starter-kit@3.21.0': + resolution: {integrity: sha512-w7fWxglDtqXFBgRYH+LforJyUboSAQllnWQbGVSTyX4rsICqZjkb3f6CTSUWpGoGKmlmbb2ZpEuoik7tur9d8Q==, tarball: https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.21.0.tgz} + + '@tiptap/suggestion@3.21.0': + resolution: {integrity: sha512-ShgbaH2hbCRLV+AaVuSx7a/nzPOSdUeW0TNeuxZfWim62MBcx33qtllja1zOoY2tgGXokEihuzFODx83IyvGfQ==, tarball: https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.21.0.tgz} + peerDependencies: + '@tiptap/core': ^3.21.0 + '@tiptap/pm': ^3.21.0 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, tarball: https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==, tarball: https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, tarball: https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==, tarball: https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, tarball: https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==, tarball: https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==, tarball: https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==, tarball: https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz} + + '@types/node@25.0.10': + resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==, tarball: https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, tarball: https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==, tarball: https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz} + + '@typescript-eslint/eslint-plugin@8.57.2': + resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.57.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.57.2': + resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.57.2': + resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==, tarball: https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.57.2': + resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.2': + resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==, tarball: https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.57.2': + resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.57.2': + resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.2': + resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.57.2': + resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.57.2': + resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==, tarball: https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz} + + '@use-gesture/vanilla@10.3.1': + resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==, tarball: https://registry.npmjs.org/@use-gesture/vanilla/-/vanilla-10.3.1.tgz} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==, tarball: https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==, tarball: https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, tarball: https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz} + engines: {node: '>= 0.4'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, tarball: https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz} + engines: {node: 18 || 20 || >=22} + + bits-ui@2.16.4: + resolution: {integrity: sha512-76dKbW0DGplG7qnVM3kljE2CmCXqh9tj9OfVo0DvC/ZGRkZNJ/auU/aQnMKBxDnSqRvsz2mZBccBkUKyF4/SoQ==, tarball: https://registry.npmjs.org/bits-ui/-/bits-ui-2.16.4.tgz} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==, tarball: https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz} + engines: {node: 18 || 20 || >=22} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, tarball: https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz} + engines: {node: '>=6'} + + contrail@https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf: + resolution: {tarball: https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf} + version: 0.0.2 + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, tarball: https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz} + engines: {node: '>=18'} + + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==, tarball: https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==, tarball: https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, tarball: https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz} + engines: {node: '>=4'} + hasBin: true + + custom-event-polyfill@1.0.7: + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==, tarball: https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, tarball: https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, tarball: https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz} + engines: {node: '>=0.10.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, tarball: https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz} + engines: {node: '>=8'} + + devalue@5.6.4: + resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==, tarball: https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==, tarball: https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz} + + dompurify@3.3.3: + resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==, tarball: https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz} + + emoji-picker-element@1.29.1: + resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==, tarball: https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.29.1.tgz} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, tarball: https://registry.npmjs.org/entities/-/entities-4.5.0.tgz} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, tarball: https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, tarball: https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==, tarball: https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-svelte@3.16.0: + resolution: {integrity: sha512-DJXxqpYZUxcE0SfYo8EJzV2ZC+zAD7fJp1n1HwcEMRR1cOEUYvjT9GuzJeNghMjgb7uxuK3IJAzI+x6zzUxO5A==, tarball: https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.16.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, tarball: https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==, tarball: https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.1.0: + resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==, tarball: https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==, tarball: https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, tarball: https://registry.npmjs.org/espree/-/espree-10.4.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==, tarball: https://registry.npmjs.org/espree/-/espree-11.2.0.tgz} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, tarball: https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz} + engines: {node: '>=0.10'} + + esrap@2.2.4: + resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==, tarball: https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, tarball: https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, tarball: https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, tarball: https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz} + engines: {node: '>=0.10.0'} + + event-target-polyfill@0.0.4: + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==, tarball: https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, tarball: https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, tarball: https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, tarball: https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, tarball: https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, tarball: https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==, tarball: https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, tarball: https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==, tarball: https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz} + engines: {node: '>=10.13.0'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==, tarball: https://registry.npmjs.org/globals/-/globals-16.5.0.tgz} + engines: {node: '>=18'} + + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==, tarball: https://registry.npmjs.org/globals/-/globals-17.4.0.tgz} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, tarball: https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==, tarball: https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz} + engines: {node: '>=12.0.0'} + + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==, tarball: https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz} + + hono@4.12.9: + resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==, tarball: https://registry.npmjs.org/hono/-/hono-4.12.9.tgz} + engines: {node: '>=16.9.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, tarball: https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==, tarball: https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, tarball: https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz} + engines: {node: '>=0.8.19'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==, tarball: https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz} + + is-emoji-supported@0.0.5: + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==, tarball: https://registry.npmjs.org/is-emoji-supported/-/is-emoji-supported-0.0.5.tgz} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, tarball: https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz} + engines: {node: '>=0.10.0'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==, tarball: https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==, tarball: https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, tarball: https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, tarball: https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, tarball: https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==, tarball: https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmjs.org/levn/-/levn-0.4.1.tgz} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, tarball: https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, tarball: https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, tarball: https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, tarball: https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, tarball: https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, tarball: https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, tarball: https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, tarball: https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz} + engines: {node: '>= 12.0.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz} + engines: {node: '>=10'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==, tarball: https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==, tarball: https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz} + + loadjs@4.3.0: + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==, tarball: https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==, tarball: https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, tarball: https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz} + engines: {node: '>=10'} + + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==, tarball: https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, tarball: https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==, tarball: https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz} + hasBin: true + + marked@17.0.5: + resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==, tarball: https://registry.npmjs.org/marked/-/marked-17.0.5.tgz} + engines: {node: '>= 20'} + hasBin: true + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==, tarball: https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==, tarball: https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz} + hasBin: true + + miniflare@4.20260317.3: + resolution: {integrity: sha512-tK78D3X4q30/SXqVwMhWrUfH+ffRou9dJLC+jkhNy5zh1I7i7T4JH6xihOvYxdCSBavJ5fQXaaxDJz6orh09BA==, tarball: https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.3.tgz} + engines: {node: '>=18.0.0'} + hasBin: true + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz} + engines: {node: 18 || 20 || >=22} + + mode-watcher@1.1.0: + resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==, tarball: https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz} + peerDependencies: + svelte: ^5.27.0 + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, tarball: https://registry.npmjs.org/mri/-/mri-1.2.0.tgz} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, tarball: https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.7: + resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, tarball: https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz} + + number-flow@0.6.0: + resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==, tarball: https://registry.npmjs.org/number-flow/-/number-flow-0.6.0.tgz} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, tarball: https://registry.npmjs.org/obug/-/obug-2.1.1.tgz} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, tarball: https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz} + engines: {node: '>= 0.8.0'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==, tarball: https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, tarball: https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, tarball: https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz} + engines: {node: '>=10'} + + partysocket@1.1.16: + resolution: {integrity: sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w==, tarball: https://registry.npmjs.org/partysocket/-/partysocket-1.1.16.tgz} + peerDependencies: + react: '>=17' + peerDependenciesMeta: + react: + optional: true + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, tarball: https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz} + engines: {node: '>=12'} + + plyr@3.8.4: + resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==, tarball: https://registry.npmjs.org/plyr/-/plyr-3.8.4.tgz} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==, tarball: https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==, tarball: https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==, tarball: https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==, tarball: https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz} + engines: {node: '>=4'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, tarball: https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.5.1: + resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==, tarball: https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==, tarball: https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, tarball: https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz} + engines: {node: '>=14'} + hasBin: true + + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==, tarball: https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==, tarball: https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==, tarball: https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==, tarball: https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==, tarball: https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==, tarball: https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==, tarball: https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==, tarball: https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==, tarball: https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==, tarball: https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==, tarball: https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==, tarball: https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==, tarball: https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==, tarball: https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==, tarball: https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==, tarball: https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.11.0: + resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==, tarball: https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz} + + prosemirror-view@1.41.7: + resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==, tarball: https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==, tarball: https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz} + engines: {node: '>=6'} + + rangetouch@2.0.1: + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==, tarball: https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz} + engines: {node: '>= 14.18.0'} + + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==, tarball: https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, tarball: https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz} + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==, tarball: https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==, tarball: https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz} + + runed@0.23.4: + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==, tarball: https://registry.npmjs.org/runed/-/runed-0.23.4.tgz} + peerDependencies: + svelte: ^5.7.0 + + runed@0.25.0: + resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==, tarball: https://registry.npmjs.org/runed/-/runed-0.25.0.tgz} + peerDependencies: + svelte: ^5.7.0 + + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==, tarball: https://registry.npmjs.org/runed/-/runed-0.28.0.tgz} + peerDependencies: + svelte: ^5.7.0 + + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==, tarball: https://registry.npmjs.org/runed/-/runed-0.35.1.tgz} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==, tarball: https://registry.npmjs.org/sade/-/sade-1.8.1.tgz} + engines: {node: '>=6'} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, tarball: https://registry.npmjs.org/semver/-/semver-7.7.4.tgz} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==, tarball: https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, tarball: https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} + engines: {node: '>=8'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, tarball: https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz} + engines: {node: '>=0.10.0'} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==, tarball: https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==, tarball: https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz} + engines: {node: '>=18'} + + svelte-check@4.4.5: + resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==, tarball: https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.6.0: + resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==, tarball: https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-sonner@1.1.0: + resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==, tarball: https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.0.tgz} + peerDependencies: + svelte: ^5.0.0 + + svelte-tiptap@3.0.1: + resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==, tarball: https://registry.npmjs.org/svelte-tiptap/-/svelte-tiptap-3.0.1.tgz} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.0.0 + '@tiptap/extension-bubble-menu': ^3.0.0 + '@tiptap/extension-floating-menu': ^3.0.0 + '@tiptap/pm': ^3.0.0 + svelte: ^5.0.0 + + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==, tarball: https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte-toolbelt@0.7.1: + resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==, tarball: https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + + svelte@5.55.0: + resolution: {integrity: sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==, tarball: https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz} + engines: {node: '>=18'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==, tarball: https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==, tarball: https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz} + + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==, tarball: https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==, tarball: https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==, tarball: https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, tarball: https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz} + engines: {node: '>=6'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, tarball: https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==, tarball: https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, tarball: https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, tarball: https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz} + engines: {node: '>=16'} + + typescript-eslint@8.57.2: + resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==, tarball: https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==, tarball: https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==, tarball: https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz} + + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==, tarball: https://registry.npmjs.org/undici/-/undici-7.24.4.tgz} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, tarball: https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz} + + unicode-segmenter@0.14.5: + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==, tarball: https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, tarball: https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz} + + url-polyfill@1.1.14: + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==, tarball: https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, tarball: https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz} + + valibot@1.3.1: + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==, tarball: https://registry.npmjs.org/valibot/-/valibot-1.3.1.tgz} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==, tarball: https://registry.npmjs.org/vite/-/vite-8.0.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.2: + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==, tarball: https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, tarball: https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz} + + web-haptics@0.0.6: + resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==, tarball: https://registry.npmjs.org/web-haptics/-/web-haptics-0.0.6.tgz} + peerDependencies: + react: '>=18' + react-dom: '>=18' + svelte: '>=4' + vue: '>=3' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + vue: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, tarball: https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz} + engines: {node: '>=0.10.0'} + + workerd@1.20260317.1: + resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==, tarball: https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz} + engines: {node: '>=16'} + hasBin: true + + worktop@0.8.0-next.18: + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==, tarball: https://registry.npmjs.org/worktop/-/worktop-0.8.0-next.18.tgz} + engines: {node: '>=12'} + + wrangler@4.78.0: + resolution: {integrity: sha512-He/vUhk4ih0D0eFmtNnlbT6Od8j+BEokaSR+oYjbVsH0SWIrIch+eHqfLRSBjBQaOoh6HCNxcafcIkBm2u0Hag==, tarball: https://registry.npmjs.org/wrangler/-/wrangler-4.78.0.tgz} + engines: {node: '>=20.3.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260317.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==, tarball: https://registry.npmjs.org/ws/-/ws-8.18.0.tgz} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==, tarball: https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz} + engines: {node: '>=12.20'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==, tarball: https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==, tarball: https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==, tarball: https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz} + +snapshots: + + '@atcute/atproto@3.1.10': + dependencies: + '@atcute/lexicons': 1.2.9 + + '@atcute/bluesky-richtext-segmenter@3.0.0': {} + + '@atcute/bluesky@3.3.0': + dependencies: + '@atcute/atproto': 3.1.10 + '@atcute/lexicons': 1.2.9 + + '@atcute/car@5.1.1': + dependencies: + '@atcute/cbor': 2.3.2 + '@atcute/cid': 2.4.1 + '@atcute/uint8array': 1.1.1 + '@atcute/varint': 2.0.0 + + '@atcute/cbor@2.3.2': + dependencies: + '@atcute/cid': 2.4.1 + '@atcute/multibase': 1.2.0 + '@atcute/uint8array': 1.1.1 + + '@atcute/cid@2.4.1': + dependencies: + '@atcute/multibase': 1.2.0 + '@atcute/uint8array': 1.1.1 + + '@atcute/client@4.2.1': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/lexicons': 1.2.9 + + '@atcute/crypto@2.4.1': + dependencies: + '@atcute/multibase': 1.2.0 + '@atcute/uint8array': 1.1.1 + '@noble/secp256k1': 3.0.0 + + '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/lexicons': 1.2.9 + '@atcute/util-fetch': 1.0.5 + '@badrap/valita': 0.4.6 + + '@atcute/identity@1.1.4': + dependencies: + '@atcute/lexicons': 1.2.9 + '@badrap/valita': 0.4.6 + + '@atcute/jetstream@1.1.2': + dependencies: + '@atcute/lexicons': 1.2.9 + '@badrap/valita': 0.4.6 + '@mary-ext/event-iterator': 1.0.0 + '@mary-ext/simple-event-emitter': 1.0.1 + partysocket: 1.1.16 + type-fest: 4.41.0 + yocto-queue: 1.2.2 + transitivePeerDependencies: + - react + + '@atcute/lex-cli@2.5.3': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/lexicon-doc': 2.1.2 + '@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4) + '@atcute/lexicons': 1.2.9 + '@badrap/valita': 0.4.6 + '@optique/core': 0.6.11 + '@optique/run': 0.6.11 + picocolors: 1.1.1 + prettier: 3.8.1 + + '@atcute/lexicon-doc@2.1.2': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/lexicons': 1.2.9 + '@atcute/uint8array': 1.1.1 + '@atcute/util-text': 1.2.0 + '@badrap/valita': 0.4.6 + + '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4)': + dependencies: + '@atcute/crypto': 2.4.1 + '@atcute/identity': 1.1.4 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/lexicon-doc': 2.1.2 + '@atcute/lexicons': 1.2.9 + '@atcute/repo': 0.1.3 + '@atcute/util-fetch': 1.0.5 + '@badrap/valita': 0.4.6 + + '@atcute/lexicons@1.2.9': + dependencies: + '@atcute/uint8array': 1.1.1 + '@atcute/util-text': 1.2.0 + '@standard-schema/spec': 1.1.0 + esm-env: 1.2.2 + + '@atcute/mst@1.0.0': + dependencies: + '@atcute/cbor': 2.3.2 + '@atcute/cid': 2.4.1 + '@atcute/uint8array': 1.1.1 + + '@atcute/multibase@1.2.0': + dependencies: + '@atcute/uint8array': 1.1.1 + + '@atcute/oauth-crypto@0.1.0': + dependencies: + '@atcute/multibase': 1.2.0 + '@atcute/uint8array': 1.1.1 + '@badrap/valita': 0.4.6 + nanoid: 5.1.7 + + '@atcute/oauth-keyset@0.1.0': + dependencies: + '@atcute/oauth-crypto': 0.1.0 + + '@atcute/oauth-node-client@1.1.0': + dependencies: + '@atcute/client': 4.2.1 + '@atcute/identity': 1.1.4 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/lexicons': 1.2.9 + '@atcute/oauth-crypto': 0.1.0 + '@atcute/oauth-keyset': 0.1.0 + '@atcute/oauth-types': 0.1.1 + '@atcute/util-fetch': 1.0.5 + '@badrap/valita': 0.4.6 + nanoid: 5.1.7 + + '@atcute/oauth-types@0.1.1': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/lexicons': 1.2.9 + '@atcute/oauth-keyset': 0.1.0 + '@badrap/valita': 0.4.6 + + '@atcute/repo@0.1.3': + dependencies: + '@atcute/car': 5.1.1 + '@atcute/cbor': 2.3.2 + '@atcute/cid': 2.4.1 + '@atcute/crypto': 2.4.1 + '@atcute/lexicons': 1.2.9 + '@atcute/mst': 1.0.0 + '@atcute/uint8array': 1.1.1 + + '@atcute/tid@1.1.2': + dependencies: + '@atcute/time-ms': 1.3.2 + + '@atcute/time-ms@1.3.2': {} + + '@atcute/uint8array@1.1.1': {} + + '@atcute/util-fetch@1.0.5': + dependencies: + '@badrap/valita': 0.4.6 + + '@atcute/util-text@1.2.0': + dependencies: + unicode-segmenter: 0.14.5 + + '@atcute/varint@2.0.0': {} + + '@badrap/valita@0.4.6': {} + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260317.1 + + '@cloudflare/workerd-darwin-64@1.20260317.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260317.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260317.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260317.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260317.1': + optional: true + + '@cloudflare/workers-types@4.20260317.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': + dependencies: + eslint: 10.1.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@2.0.3(eslint@10.1.0(jiti@2.6.1))': + dependencies: + '@eslint/core': 1.1.1 + optionalDependencies: + eslint: 10.1.0(jiti@2.6.1) + + '@eslint/config-array@0.23.3': + dependencies: + '@eslint/object-schema': 3.0.3 + debug: 4.4.3 + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.3': + dependencies: + '@eslint/core': 1.1.1 + + '@eslint/core@1.1.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': + optionalDependencies: + eslint: 10.1.0(jiti@2.6.1) + + '@eslint/object-schema@3.0.3': {} + + '@eslint/plugin-kit@0.6.1': + dependencies: + '@eslint/core': 1.1.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@foxui/core@0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2)': + dependencies: + '@number-flow/svelte': 0.4.0(svelte@5.55.0) + bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + clsx: 2.1.1 + dompurify: 3.3.3 + mode-watcher: 1.1.0(svelte@5.55.0) + svelte: 5.55.0 + svelte-sonner: 1.1.0(svelte@5.55.0) + tailwind-merge: 3.5.0 + tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) + tailwindcss: 4.2.2 + web-haptics: 0.0.6(svelte@5.55.0) + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue + + '@foxui/social@0.8.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2)': + dependencies: + '@atcute/bluesky': 3.3.0 + '@atcute/bluesky-richtext-segmenter': 3.0.0 + '@foxui/core': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) + '@foxui/text': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2) + '@foxui/time': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) + '@use-gesture/vanilla': 10.3.1 + bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + emoji-picker-element: 1.29.1 + hls.js: 1.6.15 + is-emoji-supported: 0.0.5 + plyr: 3.8.4 + svelte: 5.55.0 + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - '@tiptap/extension-code-block' + - '@tiptap/extensions' + - highlight.js + - react + - react-dom + - vue + + '@foxui/text@0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@foxui/core': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/extension-bubble-menu': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-code-block-lowlight': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(highlight.js@11.11.1)(lowlight@3.3.0) + '@tiptap/extension-document': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-dropcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-floating-menu': 3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-history': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-image': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-link': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-paragraph': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-placeholder': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-strike': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-text': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-typography': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-underline': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/markdown': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + '@tiptap/starter-kit': 3.21.0 + '@tiptap/suggestion': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + lowlight: 3.3.0 + state: link:@tiptap/pm/state + svelte: 5.55.0 + svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(svelte@5.55.0) + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - '@tiptap/extension-code-block' + - '@tiptap/extensions' + - highlight.js + - react + - react-dom + - vue + + '@foxui/time@0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2)': + dependencies: + '@foxui/core': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) + '@number-flow/svelte': 0.4.0(svelte@5.55.0) + bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + svelte: 5.55.0 + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.9.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@internationalized/date@3.10.1': + dependencies: + '@swc/helpers': 0.5.20 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mary-ext/event-iterator@1.0.0': + dependencies: + yocto-queue: 1.2.2 + + '@mary-ext/simple-event-emitter@1.0.1': {} + + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@noble/secp256k1@3.0.0': {} + + '@number-flow/svelte@0.4.0(svelte@5.55.0)': + dependencies: + esm-env: 1.2.2 + number-flow: 0.6.0 + svelte: 5.55.0 + + '@optique/core@0.6.11': {} + + '@optique/run@0.6.11': + dependencies: + '@optique/core': 0.6.11 + + '@oxc-project/types@0.122.0': {} + + '@polka/url@1.0.0-next.29': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@remirror/core-constants@3.0.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.15': {} + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.78.0(@cloudflare/workers-types@4.20260317.1))': + dependencies: + '@cloudflare/workers-types': 4.20260317.1 + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + worktop: 0.8.0-next.18 + wrangler: 4.78.0(@cloudflare/workers-types@4.20260317.1) + + '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@types/cookie': 0.6.0 + acorn: 8.16.0 + cookie: 0.6.0 + devalue: 5.6.4 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.1.0 + sirv: 3.0.2 + svelte: 5.55.0 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + optionalDependencies: + typescript: 6.0.2 + + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.55.0 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vitefu: 1.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + + '@swc/helpers@0.5.20': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.2)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.2.2 + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + + '@tiptap/core@3.21.0(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/pm': 3.21.0 + + '@tiptap/extension-blockquote@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-bold@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/extension-bullet-list@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-code-block-lowlight@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(highlight.js@11.11.1)(lowlight@3.3.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/extension-code-block': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + highlight.js: 11.11.1 + lowlight: 3.3.0 + + '@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/extension-code@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-document@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-dropcursor@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/extension-gapcursor@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-hard-break@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-heading@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-history@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-horizontal-rule@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/extension-image@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-italic@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-link@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-list-keymap@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/extension-ordered-list@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-paragraph@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-placeholder@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + + '@tiptap/extension-strike@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-text@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-typography@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extension-underline@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + + '@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/markdown@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + marked: 17.0.5 + + '@tiptap/pm@3.21.0': + dependencies: + prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.3.0 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + '@tiptap/starter-kit@3.21.0': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/extension-blockquote': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-bold': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-bullet-list': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-code': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-code-block': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-document': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-dropcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-gapcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-hard-break': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-heading': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-horizontal-rule': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-italic': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-link': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-list-item': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-list-keymap': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-ordered-list': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) + '@tiptap/extension-paragraph': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-strike': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-text': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extension-underline': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) + '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tiptap/suggestion@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': + dependencies: + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/cookie@0.6.0': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@types/node@25.0.10': + dependencies: + undici-types: 7.16.0 + optional: true + + '@types/trusted-types@2.0.7': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.57.2 + eslint: 10.1.0(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.57.2 + debug: 4.4.3 + eslint: 10.1.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.57.2(typescript@6.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@6.0.2) + '@typescript-eslint/types': 8.57.2 + debug: 4.4.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.57.2': + dependencies: + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 + + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + debug: 4.4.3 + eslint: 10.1.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.57.2': {} + + '@typescript-eslint/typescript-estree@8.57.2(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.57.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@6.0.2) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + eslint: 10.1.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.57.2': + dependencies: + '@typescript-eslint/types': 8.57.2 + eslint-visitor-keys: 5.0.1 + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/vanilla@10.3.1': + dependencies: + '@use-gesture/core': 10.3.1 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + argparse@2.0.1: {} + + aria-query@5.3.1: {} + + axobject-query@4.1.0: {} + + balanced-match@4.0.4: {} + + bits-ui@2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0): + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/dom': 1.7.6 + '@internationalized/date': 3.10.1 + esm-env: 1.2.2 + runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + svelte: 5.55.0 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@sveltejs/kit' + + blake3-wasm@2.1.5: {} + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + contrail@https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf(@atcute/identity@1.1.4): + dependencies: + '@atcute/atproto': 3.1.10 + '@atcute/client': 4.2.1 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/jetstream': 1.1.2 + '@atcute/lexicons': 1.2.9 + hono: 4.12.9 + transitivePeerDependencies: + - '@atcute/identity' + - react + + cookie@0.6.0: {} + + cookie@1.1.1: {} + + core-js@3.49.0: {} + + crelt@1.0.6: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + custom-event-polyfill@1.0.7: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devalue@5.6.4: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dompurify@3.3.3: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + emoji-picker-element@1.29.1: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + entities@4.5.0: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)): + dependencies: + eslint: 10.1.0(jiti@2.6.1) + + eslint-plugin-svelte@3.16.0(eslint@10.1.0(jiti@2.6.1))(svelte@5.55.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 10.1.0(jiti@2.6.1) + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.8 + postcss-load-config: 3.1.4(postcss@8.5.8) + postcss-safe-parser: 7.0.1(postcss@8.5.8) + semver: 7.7.4 + svelte-eslint-parser: 1.6.0(svelte@5.55.0) + optionalDependencies: + svelte: 5.55.0 + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.1.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@typescript-eslint/types': 8.57.2 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + event-target-polyfill@0.0.4: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@16.5.0: {} + + globals@17.4.0: {} + + graceful-fs@4.2.11: {} + + highlight.js@11.11.1: {} + + hls.js@1.6.15: {} + + hono@4.12.9: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + inline-style-parser@0.2.7: {} + + is-emoji-supported@0.0.5: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + isexe@2.0.0: {} + + jiti@2.6.1: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + known-css-properties@0.37.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lilconfig@2.1.0: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + + loadjs@4.3.0: {} + + locate-character@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + marked@17.0.5: {} + + mdurl@2.0.0: {} + + mini-svg-data-uri@1.4.4: {} + + miniflare@4.20260317.3: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.4 + workerd: 1.20260317.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.5 + + mode-watcher@1.1.0(svelte@5.55.0): + dependencies: + runed: 0.25.0(svelte@5.55.0) + svelte: 5.55.0 + svelte-toolbelt: 0.7.1(svelte@5.55.0) + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + nanoid@5.1.7: {} + + natural-compare@1.4.0: {} + + number-flow@0.6.0: + dependencies: + esm-env: 1.2.2 + + obug@2.1.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + orderedmap@2.1.1: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + partysocket@1.1.16: + dependencies: + event-target-polyfill: 0.0.4 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + plyr@3.8.4: + dependencies: + core-js: 3.49.0 + custom-event-polyfill: 1.0.7 + loadjs: 4.3.0 + rangetouch: 2.0.1 + url-polyfill: 1.1.14 + + postcss-load-config@3.1.4(postcss@8.5.8): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.3 + optionalDependencies: + postcss: 8.5.8 + + postcss-safe-parser@7.0.1(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-scss@4.0.9(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.0): + dependencies: + prettier: 3.8.1 + svelte: 5.55.0 + + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.0))(prettier@3.8.1): + dependencies: + prettier: 3.8.1 + optionalDependencies: + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.0) + + prettier@3.8.1: {} + + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.11.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.7 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.7 + + prosemirror-transform@1.11.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.7: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + rangetouch@2.0.1: {} + + readdirp@4.1.2: {} + + regexparam@3.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + rope-sequence@1.3.4: {} + + runed@0.23.4(svelte@5.55.0): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.0 + + runed@0.25.0(svelte@5.55.0): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.0 + + runed@0.28.0(svelte@5.55.0): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.0 + + runed@0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.55.0 + optionalDependencies: + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + semver@7.7.4: {} + + set-cookie-parser@3.1.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + supports-color@10.2.2: {} + + svelte-check@4.4.5(picomatch@4.0.4)(svelte@5.55.0)(typescript@6.0.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.55.0 + typescript: 6.0.2 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.6.0(svelte@5.55.0): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.8 + postcss-scss: 4.0.9(postcss@8.5.8) + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + optionalDependencies: + svelte: 5.55.0 + + svelte-sonner@1.1.0(svelte@5.55.0): + dependencies: + runed: 0.28.0(svelte@5.55.0) + svelte: 5.55.0 + + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(svelte@5.55.0): + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) + '@tiptap/extension-bubble-menu': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/extension-floating-menu': 3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) + '@tiptap/pm': 3.21.0 + svelte: 5.55.0 + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) + style-to-object: 1.0.14 + svelte: 5.55.0 + transitivePeerDependencies: + - '@sveltejs/kit' + + svelte-toolbelt@0.7.1(svelte@5.55.0): + dependencies: + clsx: 2.1.1 + runed: 0.23.4(svelte@5.55.0) + style-to-object: 1.0.14 + svelte: 5.55.0 + + svelte@5.55.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.4 + esm-env: 1.2.2 + esrap: 2.2.4 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + tabbable@6.4.0: {} + + tailwind-merge@3.5.0: {} + + tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2): + dependencies: + tailwindcss: 4.2.2 + optionalDependencies: + tailwind-merge: 3.5.0 + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + totalist@3.0.1: {} + + ts-api-utils@2.5.0(typescript@6.0.2): + dependencies: + typescript: 6.0.2 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typescript-eslint@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.1.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + typescript@6.0.2: {} + + uc.micro@2.1.0: {} + + undici-types@7.16.0: + optional: true + + undici@7.24.4: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unicode-segmenter@0.14.5: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-polyfill@1.1.14: {} + + util-deprecate@1.0.2: {} + + valibot@1.3.1(typescript@6.0.2): + optionalDependencies: + typescript: 6.0.2 + + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.10 + esbuild: 0.27.4 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vitefu@1.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + optionalDependencies: + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + + w3c-keyname@2.2.8: {} + + web-haptics@0.0.6(svelte@5.55.0): + optionalDependencies: + svelte: 5.55.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + workerd@1.20260317.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260317.1 + '@cloudflare/workerd-darwin-arm64': 1.20260317.1 + '@cloudflare/workerd-linux-64': 1.20260317.1 + '@cloudflare/workerd-linux-arm64': 1.20260317.1 + '@cloudflare/workerd-windows-64': 1.20260317.1 + + worktop@0.8.0-next.18: + dependencies: + mrmime: 2.0.1 + regexparam: 3.0.0 + + wrangler@4.78.0(@cloudflare/workers-types@4.20260317.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260317.3 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260317.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260317.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + yaml@1.10.3: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 + + zimmerframe@1.1.4: {} diff --git a/examples/sveltekit-group-chat/scripts/append-scheduled.ts b/examples/sveltekit-group-chat/scripts/append-scheduled.ts new file mode 100644 index 0000000..c9bd1cd --- /dev/null +++ b/examples/sveltekit-group-chat/scripts/append-scheduled.ts @@ -0,0 +1,29 @@ +/** + * Post-build script: appends a `scheduled` handler to the SvelteKit worker output. + * + * SvelteKit's adapter-cloudflare doesn't support the `scheduled` export natively + * (see https://github.com/sveltejs/kit/issues/4841). This script patches the + * generated _worker.js to add one that self-calls the /api/cron endpoint. + */ +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const workerPath = join(root, '.svelte-kit', 'cloudflare', '_worker.js'); + +let code = readFileSync(workerPath, 'utf-8'); + +code += ` +// --- Appended by scripts/append-scheduled.ts --- +worker_default.scheduled = async function (event, env, ctx) { + const req = new Request('http://localhost/api/cron', { + method: 'POST', + headers: { 'X-Cron-Secret': env.CRON_SECRET || '' } + }); + ctx.waitUntil(this.fetch(req, env, ctx)); +}; +`; + +writeFileSync(workerPath, code); +console.log('Appended scheduled handler to _worker.js'); diff --git a/examples/sveltekit-group-chat/scripts/generate.ts b/examples/sveltekit-group-chat/scripts/generate.ts new file mode 100644 index 0000000..5a1cb3a --- /dev/null +++ b/examples/sveltekit-group-chat/scripts/generate.ts @@ -0,0 +1,13 @@ +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { config } from '../src/lib/contrail/config'; +import { generateLexicons } from '@atmo-dev/contrail/generate'; + +const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); + +generateLexicons({ + config, + rootDir: ROOT_DIR, + outputDir: join(ROOT_DIR, 'lexicons-generated'), + writeRuntimeFiles: true +}); diff --git a/examples/sveltekit-group-chat/scripts/sync.ts b/examples/sveltekit-group-chat/scripts/sync.ts new file mode 100644 index 0000000..c946f86 --- /dev/null +++ b/examples/sveltekit-group-chat/scripts/sync.ts @@ -0,0 +1,68 @@ +/** + * Discover users from relays and backfill their records from PDS. + * + * Usage: + * pnpm sync # local D1 + * pnpm sync:remote # prod D1 + */ +import { Contrail } from '@atmo-dev/contrail'; +import { config } from '../src/lib/contrail/config'; +import { getPlatformProxy } from 'wrangler'; + +function elapsed(start: number): string { + const ms = Date.now() - start; + if (ms < 1000) return `${ms}ms`; + if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; + const mins = Math.floor(ms / 60_000); + const secs = ((ms % 60_000) / 1000).toFixed(0); + return `${mins}m ${secs}s`; +} + +async function main() { + const remote = process.argv.includes('--remote'); + const syncStart = Date.now(); + + console.log(`=== Sync (${remote ? 'remote/prod' : 'local'} D1) ===\n`); + + const { env, dispose } = await getPlatformProxy<{ DB: D1Database }>({ + environment: remote ? 'production' : undefined + }); + + const contrail = new Contrail({ ...config, db: env.DB }); + + try { + await contrail.init(); + + console.log('--- Discovery ---'); + const discoveryStart = Date.now(); + const discovered = await contrail.discover(); + console.log(` Done: ${discovered.length} users in ${elapsed(discoveryStart)}\n`); + + console.log('--- Backfill ---'); + const backfillStart = Date.now(); + const total = await contrail.backfill({ + concurrency: 100, + onProgress: ({ records, usersComplete, usersTotal, usersFailed }) => { + const secs = (Date.now() - backfillStart) / 1000; + const rate = secs > 0 ? Math.round(records / secs) : 0; + const failStr = usersFailed > 0 ? ` | ${usersFailed} failed` : ''; + process.stdout.write( + `\r ${records} records | ${usersComplete}/${usersTotal} users | ${rate}/s | ${elapsed(backfillStart)}${failStr} ` + ); + } + }); + process.stdout.write('\n'); + console.log(` Done: ${total} records in ${elapsed(backfillStart)}\n`); + + console.log(`=== Finished in ${elapsed(syncStart)} ===`); + console.log(` Discovered: ${discovered.length} users`); + console.log(` Backfilled: ${total} records`); + } finally { + await dispose(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/sveltekit-group-chat/src/app.css b/examples/sveltekit-group-chat/src/app.css new file mode 100644 index 0000000..6289b7f --- /dev/null +++ b/examples/sveltekit-group-chat/src/app.css @@ -0,0 +1,9 @@ +@import 'tailwindcss'; + +@plugin '@tailwindcss/forms'; + +@custom-variant dark (&:where(.dark, .dark *)); + +@import '@foxui/core/theme.css'; + +@source "../node_modules/@foxui"; diff --git a/examples/sveltekit-group-chat/src/app.d.ts b/examples/sveltekit-group-chat/src/app.d.ts new file mode 100644 index 0000000..831d778 --- /dev/null +++ b/examples/sveltekit-group-chat/src/app.d.ts @@ -0,0 +1,34 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +import type { OAuthSession } from '@atcute/oauth-node-client'; +import type { Client } from '@atcute/client'; +import type { Did } from '@atcute/lexicons'; + +declare global { + namespace App { + // interface Error {} + interface Locals { + session: OAuthSession | null; + client: Client | null; + did: Did | null; + } + // interface PageData {} + // interface PageState {} + interface Platform { + env: { + OAUTH_SESSIONS: KVNamespace; + OAUTH_STATES: KVNamespace; + CLIENT_ASSERTION_KEY: string; + COOKIE_SECRET: string; + OAUTH_PUBLIC_URL: string; + PROFILE_CACHE?: KVNamespace; + DB: D1Database; + CRON_SECRET: string; + }; + } + } +} +import type {} from '@atcute/atproto'; +import type {} from '@atcute/bluesky'; + +export {}; diff --git a/examples/sveltekit-group-chat/src/app.html b/examples/sveltekit-group-chat/src/app.html new file mode 100644 index 0000000..03c33ff --- /dev/null +++ b/examples/sveltekit-group-chat/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/sveltekit-group-chat/src/hooks.server.ts b/examples/sveltekit-group-chat/src/hooks.server.ts new file mode 100644 index 0000000..98b4fae --- /dev/null +++ b/examples/sveltekit-group-chat/src/hooks.server.ts @@ -0,0 +1,15 @@ +import type { Handle } from '@sveltejs/kit'; +import { restoreSession } from '$lib/atproto/server/session'; + +export const handle: Handle = async ({ event, resolve }) => { + const { session, client, did } = await restoreSession( + event.cookies, + event.platform?.env + ); + + event.locals.session = session; + event.locals.client = client; + event.locals.did = did; + + return resolve(event); +}; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/auth.svelte.ts b/examples/sveltekit-group-chat/src/lib/atproto/auth.svelte.ts new file mode 100644 index 0000000..2859236 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/auth.svelte.ts @@ -0,0 +1,75 @@ +import { AppBskyActorDefs } from '@atcute/bluesky'; +import type { ActorIdentifier, Did } from '@atcute/lexicons'; +import { page } from '$app/state'; +import { ALLOW_SIGNUP, REDIRECT_TO_LAST_PAGE_ON_LOGIN } from './settings'; + +export const user = { + get profile() { + return (page.data?.profile as AppBskyActorDefs.ProfileViewDetailed | null) ?? null; + }, + get isLoggedIn() { + return !!page.data?.did; + }, + get did() { + return (page.data?.did as Did | null) ?? null; + } +}; + +function saveReturnTo() { + if (REDIRECT_TO_LAST_PAGE_ON_LOGIN) { + document.cookie = `oauth_return_to=${encodeURIComponent(window.location.pathname + window.location.search)};path=/;max-age=600;samesite=lax`; + } +} + +export async function login(handle: string) { + if (handle.startsWith('did:')) { + if (handle.length < 6) throw new Error('DID must be at least 6 characters'); + } else if (handle.includes('.') && handle.length > 3) { + handle = (handle.startsWith('@') ? handle.slice(1) : handle) as ActorIdentifier; + if (handle.length < 4) throw new Error('Handle must be at least 4 characters'); + } else if (handle.length > 3) { + handle = ((handle.startsWith('@') ? handle.slice(1) : handle) + + '.bsky.social') as ActorIdentifier; + } else { + throw new Error('Please provide a valid handle or DID.'); + } + + const { oauthLogin } = await import('./server/oauth.remote'); + const { url } = await oauthLogin({ handle }); + saveReturnTo(); + window.location.assign(url); + + // Wait for navigation (prevents UI flash) + await new Promise((_resolve, reject) => { + window.addEventListener('pageshow', () => reject(new Error('user aborted the login request')), { + once: true + }); + }); +} + +export async function signup() { + if (!ALLOW_SIGNUP) throw new Error('Signup is not enabled'); + + const { oauthLogin } = await import('./server/oauth.remote'); + const { url } = await oauthLogin({ signup: true }); + saveReturnTo(); + window.location.assign(url); + + await new Promise((_resolve, reject) => { + window.addEventListener('pageshow', () => reject(new Error('user aborted the signup request')), { + once: true + }); + }); +} + +export async function logout() { + try { + const { oauthLogout } = await import('./server/oauth.remote'); + await oauthLogout(); + } catch (e) { + console.error('Error logging out:', e); + } + + // Full reload to clear server session state + window.location.href = '/'; +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/image-helper.ts b/examples/sveltekit-group-chat/src/lib/atproto/image-helper.ts new file mode 100644 index 0000000..08ddf93 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/image-helper.ts @@ -0,0 +1,152 @@ +import { getCDNImageBlobUrl, uploadBlob } from './methods'; + +export function compressImage( + file: File | Blob, + maxSize: number = 900 * 1024, + maxDimension: number = 2048 +): Promise<{ + blob: Blob; + aspectRatio: { + width: number; + height: number; + }; +}> { + return new Promise((resolve, reject) => { + const img = new Image(); + const reader = new FileReader(); + + reader.onload = (e) => { + if (!e.target?.result) { + return reject(new Error('Failed to read file.')); + } + img.src = e.target.result as string; + }; + + reader.onerror = (err) => reject(err); + reader.readAsDataURL(file); + + img.onload = () => { + let width = img.width; + let height = img.height; + + // If image is already small enough, return original + if (file.size <= maxSize) { + console.log('skipping compression+resizing, already small enough'); + return resolve({ + blob: file, + aspectRatio: { + width, + height + } + }); + } + + if (width > maxDimension || height > maxDimension) { + if (width > height) { + height = Math.round((maxDimension / width) * height); + width = maxDimension; + } else { + width = Math.round((maxDimension / height) * width); + height = maxDimension; + } + } + + // Create a canvas to draw the image + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) return reject(new Error('Failed to get canvas context.')); + ctx.drawImage(img, 0, 0, width, height); + + // Use WebP for both compression and transparency support + let quality = 0.9; + + function attemptCompression() { + canvas.toBlob( + (blob) => { + if (!blob) { + return reject(new Error('Compression failed.')); + } + if (blob.size <= maxSize || quality < 0.3) { + resolve({ + blob, + aspectRatio: { + width, + height + } + }); + } else { + quality -= 0.1; + attemptCompression(); + } + }, + 'image/webp', + quality + ); + } + + attemptCompression(); + }; + + img.onerror = (err) => reject(err); + }); +} + +export async function checkAndUploadImage( + recordWithImage: Record, + key: string = 'image', + // e.g. /api/image-proxy?url= + imageProxy?: string +) { + if (!recordWithImage[key]) return; + + // Already uploaded as blob + if (typeof recordWithImage[key] === 'object' && recordWithImage[key].$type === 'blob') { + return; + } + + if (typeof recordWithImage[key] === 'string' && imageProxy) { + const proxyUrl = imageProxy + encodeURIComponent(recordWithImage[key]); + const response = await fetch(proxyUrl); + if (!response.ok) { + throw Error('failed to get image from image proxy'); + } + + const blob = await response.blob(); + const { blob: compressed, aspectRatio } = await compressImage(blob); + + recordWithImage[key] = await uploadBlob({ blob: compressed, aspectRatio }); + + return; + } + + if (recordWithImage[key]?.blob) { + if (recordWithImage[key].objectUrl) { + URL.revokeObjectURL(recordWithImage[key].objectUrl); + } + const { blob: compressed, aspectRatio } = await compressImage(recordWithImage[key].blob); + recordWithImage[key] = await uploadBlob({ blob: compressed, aspectRatio }); + } +} + +export function getImageFromRecord( + recordWithImage: Record | undefined, + did: string, + key: string = 'image' +): string | undefined { + if (!recordWithImage?.[key]) return; + + if (typeof recordWithImage[key] === 'object' && recordWithImage[key].$type === 'blob') { + return getCDNImageBlobUrl({ did, blob: recordWithImage[key] }); + } + + if (recordWithImage[key].objectUrl) return recordWithImage[key].objectUrl; + + if (recordWithImage[key].blob) { + recordWithImage[key].objectUrl = URL.createObjectURL(recordWithImage[key].blob); + return recordWithImage[key].objectUrl; + } + + return recordWithImage[key]; +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/index.ts b/examples/sveltekit-group-chat/src/lib/atproto/index.ts new file mode 100644 index 0000000..8d3232a --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/index.ts @@ -0,0 +1,20 @@ +export { user, login, signup, logout } from './auth.svelte'; + +export { + parseUri, + resolveHandle, + actorToDid, + getPDS, + getDetailedProfile, + getClient, + listRecords, + getRecord, + putRecord, + deleteRecord, + uploadBlob, + describeRepo, + getBlobURL, + getCDNImageBlobUrl, + searchActorsTypeahead, + createTID +} from './methods'; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/methods.ts b/examples/sveltekit-group-chat/src/lib/atproto/methods.ts new file mode 100644 index 0000000..4a84fd5 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/methods.ts @@ -0,0 +1,373 @@ +import { parseResourceUri, type Did, type Handle } from '@atcute/lexicons'; +import { isDid } from '@atcute/lexicons/syntax'; +import { user } from './auth.svelte'; +import { DOH_RESOLVER, type AllowedCollection } from './settings'; +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver +} from '@atcute/identity-resolver'; +import { Client, simpleFetchHandler } from '@atcute/client'; +import { type AppBskyActorDefs } from '@atcute/bluesky'; + +export type Collection = `${string}.${string}.${string}`; +import * as TID from '@atcute/tid'; + +/** + * Parses an AT Protocol URI into its components. + */ +export function parseUri(uri: string) { + const parts = parseResourceUri(uri); + if (!parts.ok) return; + return parts.value; +} + +/** + * Resolves a handle to a DID using DNS and HTTP methods. + */ +export async function resolveHandle({ handle }: { handle: Handle }) { + const handleResolver = new CompositeHandleResolver({ + methods: { + dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), + http: new WellKnownHandleResolver() + } + }); + + const data = await handleResolver.resolve(handle); + return data; +} + +/** + * Returns a DID given a handle or DID string. + */ +export async function actorToDid(actor: string): Promise { + if (isDid(actor)) return actor; + return await resolveHandle({ handle: actor as Handle }); +} + +const didResolver = new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver() + } +}); + +/** + * Gets the PDS (Personal Data Server) URL for a given DID. + */ +export async function getPDS(did: Did) { + const doc = await didResolver.resolve(did as Did<'plc'> | Did<'web'>); + if (!doc.service) throw new Error('No PDS found'); + for (const service of doc.service) { + if (service.id === '#atproto_pds') { + return service.serviceEndpoint.toString(); + } + } +} + +/** + * Fetches a detailed Bluesky profile for a user. + */ +export async function getDetailedProfile(data?: { did?: Did; client?: Client }) { + data ??= {}; + data.did ??= user.did ?? undefined; + + if (!data.did) throw new Error('Error getting detailed profile: no did'); + + data.client ??= new Client({ + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) + }); + + const response = await data.client.get('app.bsky.actor.getProfile', { + params: { actor: data.did } + }); + + if (!response.ok) return; + + return response.data; +} + +/** + * Creates an AT Protocol client for a user's PDS. + */ +export async function getClient({ did }: { did: Did }) { + const pds = await getPDS(did); + if (!pds) throw new Error('PDS not found'); + + const client = new Client({ + handler: simpleFetchHandler({ service: pds }) + }); + + return client; +} + +/** + * Lists records from a repository collection with pagination support. + */ +export async function listRecords({ + did, + collection, + cursor, + limit = 100, + client +}: { + did?: Did; + collection: `${string}.${string}.${string}`; + cursor?: string; + limit?: number; + client?: Client; +}) { + did ??= user.did ?? undefined; + if (!collection) { + throw new Error('Missing parameters for listRecords'); + } + if (!did) { + throw new Error('Missing did for listRecords'); + } + + client ??= await getClient({ did }); + + const allRecords = []; + + let currentCursor = cursor; + do { + const response = await client.get('com.atproto.repo.listRecords', { + params: { + repo: did, + collection, + limit: !limit || limit > 100 ? 100 : limit, + cursor: currentCursor + } + }); + + if (!response.ok) { + return allRecords; + } + + allRecords.push(...response.data.records); + currentCursor = response.data.cursor; + } while (currentCursor && (!limit || allRecords.length < limit)); + + return allRecords; +} + +/** + * Fetches a single record from a repository. + */ +export async function getRecord({ + did, + collection, + rkey = 'self', + client +}: { + did?: Did; + collection: Collection; + rkey?: string; + client?: Client; +}) { + did ??= user.did ?? undefined; + + if (!collection) { + throw new Error('Missing parameters for getRecord'); + } + if (!did) { + throw new Error('Missing did for getRecord'); + } + + client ??= await getClient({ did }); + + const record = await client.get('com.atproto.repo.getRecord', { + params: { + repo: did, + collection, + rkey + } + }); + + return JSON.parse(JSON.stringify(record.data)); +} + +/** + * Creates or updates a record via remote function. + */ +export async function putRecord({ + collection, + rkey = 'self', + record +}: { + collection: AllowedCollection; + rkey?: string; + record: Record; +}) { + if (!user.did) throw new Error('Not logged in'); + + const { putRecord: putRecordRemote } = await import('./server/repo.remote'); + const data = await putRecordRemote({ collection, rkey, record }); + return { ok: true, data }; +} + +/** + * Deletes a record via remote function. + */ +export async function deleteRecord({ + collection, + rkey = 'self' +}: { + collection: AllowedCollection; + rkey: string; +}) { + if (!user.did) throw new Error('Not logged in'); + + const { deleteRecord: deleteRecordRemote } = await import('./server/repo.remote'); + const data = await deleteRecordRemote({ collection, rkey }); + return data.ok; +} + +/** + * Gets the dimensions of an image blob. + */ +function getImageDimensions(blob: Blob): Promise<{ width: number; height: number }> { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(blob); + img.onload = () => { + URL.revokeObjectURL(url); + resolve({ width: img.naturalWidth, height: img.naturalHeight }); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Failed to load image for dimensions')); + }; + img.src = url; + }); +} + +/** + * Uploads a blob via remote function. + * Converts the Blob to a byte array for serialization across the remote boundary. + * For image blobs, automatically includes aspectRatio with width and height. + */ +export async function uploadBlob({ + blob, + aspectRatio +}: { + blob: Blob; + aspectRatio?: { width: number; height: number }; +}) { + if (!user.did) throw new Error("Can't upload blob: Not logged in"); + + // Auto-detect dimensions for image blobs if not provided + if (!aspectRatio && blob.type.startsWith('image/')) { + try { + aspectRatio = await getImageDimensions(blob); + } catch { + // Non-critical — proceed without aspectRatio + } + } + + const arrayBuffer = await blob.arrayBuffer(); + const bytes = Array.from(new Uint8Array(arrayBuffer)); + + const { uploadBlob: uploadBlobRemote } = await import('./server/repo.remote'); + const result = await uploadBlobRemote({ bytes, mimeType: blob.type || 'application/octet-stream' }); + + if (aspectRatio) { + return { ...result, aspectRatio }; + } + return result; +} + +/** + * Gets metadata about a repository. + */ +export async function describeRepo({ client, did }: { client?: Client; did?: Did }) { + did ??= user.did ?? undefined; + if (!did) { + throw new Error('Error describeRepo: No did'); + } + client ??= await getClient({ did }); + + const repo = await client.get('com.atproto.repo.describeRepo', { + params: { + repo: did + } + }); + if (!repo.ok) return; + + return repo.data; +} + +/** + * Constructs a URL to fetch a blob directly from a user's PDS. + */ +export async function getBlobURL({ + did, + blob +}: { + did: Did; + blob: { + $type: 'blob'; + ref: { + $link: string; + }; + }; +}) { + const pds = await getPDS(did); + return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${blob.ref.$link}`; +} + +/** + * Constructs a Bluesky CDN URL for an image blob. + */ +export function getCDNImageBlobUrl({ + did, + blob +}: { + did?: string; + blob: { + $type: 'blob'; + ref: { + $link: string; + }; + }; +}) { + did ??= user.did ?? undefined; + + return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`; +} + +/** + * Searches for actors with typeahead/autocomplete functionality. + */ +export async function searchActorsTypeahead( + q: string, + limit: number = 10, + host?: string +): Promise<{ actors: AppBskyActorDefs.ProfileViewBasic[]; q: string }> { + host ??= 'https://public.api.bsky.app'; + + const client = new Client({ + handler: simpleFetchHandler({ service: host }) + }); + + const response = await client.get('app.bsky.actor.searchActorsTypeahead', { + params: { + q, + limit + } + }); + + if (!response.ok) return { actors: [], q }; + + return { actors: response.data.actors, q }; +} + +/** + * Return a TID based on current time + */ +export function createTID() { + return TID.now(); +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/port.ts b/examples/sveltekit-group-chat/src/lib/atproto/port.ts new file mode 100644 index 0000000..4d6686b --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/port.ts @@ -0,0 +1,3 @@ +// Dev server port — generated by setup-dev, shared by vite, oauth, and tunnel. +// Each project gets a unique random port (5200–7200) so multiple projects can run simultaneously. +export const DEV_PORT = 5291; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-key.ts b/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-key.ts new file mode 100644 index 0000000..3d1cc8a --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-key.ts @@ -0,0 +1,4 @@ +import { generateClientAssertionKey } from '@atcute/oauth-node-client'; + +const key = await generateClientAssertionKey('main-key'); +console.log(JSON.stringify(key)); diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-secret.ts b/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-secret.ts new file mode 100644 index 0000000..9092066 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-secret.ts @@ -0,0 +1,3 @@ +import { randomBytes } from 'node:crypto'; + +console.log(randomBytes(32).toString('base64url')); diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/setup-dev.ts b/examples/sveltekit-group-chat/src/lib/atproto/scripts/setup-dev.ts new file mode 100644 index 0000000..bdc8ccf --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/scripts/setup-dev.ts @@ -0,0 +1,60 @@ +import { existsSync } from 'node:fs'; +import { copyFile, readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { randomBytes, randomInt } from 'node:crypto'; + +import { generateClientAssertionKey } from '@atcute/oauth-node-client'; + +const cwd = process.cwd(); +const examplePath = resolve(cwd, '.env.example'); +const envPath = resolve(cwd, '.env'); + +if (!existsSync(envPath)) { + if (!existsSync(examplePath)) { + throw new Error(`missing .env.example (expected at ${examplePath})`); + } + await copyFile(examplePath, envPath); + console.log(`created ${envPath}`); +} + +const upsertVar = (input: string, key: string, value: string): string => { + const line = `${key}=${value}`; + const re = new RegExp(`^${key}=.*$`, 'm'); + + if (re.test(input)) { + const match = input.match(re); + const current = match ? match[0].slice(key.length + 1).trim() : ''; + // Only overwrite if empty/placeholder + if (current === '' || current === "''" || current === '""' || current.includes('...')) { + return input.replace(re, line); + } + return input; + } + + const suffix = input.endsWith('\n') || input.length === 0 ? '' : '\n'; + return `${input}${suffix}${line}\n`; +}; + +let vars = await readFile(envPath, 'utf8'); + +const secret = randomBytes(32).toString('base64url'); +vars = upsertVar(vars, 'COOKIE_SECRET', secret); + +const jwk = await generateClientAssertionKey('main-key'); +vars = upsertVar(vars, 'CLIENT_ASSERTION_KEY', JSON.stringify(jwk)); + +await writeFile(envPath, vars); +console.log(`updated ${envPath}`); + +// Generate a random dev port (5200–7200) so multiple projects can run simultaneously +const portPath = resolve(cwd, 'src/lib/atproto/port.ts'); +const portFile = await readFile(portPath, 'utf8'); +const currentPort = portFile.match(/DEV_PORT\s*=\s*(\d+)/); +if (currentPort && parseInt(currentPort[1]) === 5183) { + const port = randomInt(5200, 7201); + const updated = portFile.replace(/DEV_PORT\s*=\s*\d+/, `DEV_PORT = ${port}`); + await writeFile(portPath, updated); + console.log(`set DEV_PORT to ${port} in port.ts`); +} else { + console.log(`DEV_PORT already customized (${currentPort?.[1]}), skipping`); +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/tunnel.ts b/examples/sveltekit-group-chat/src/lib/atproto/scripts/tunnel.ts new file mode 100644 index 0000000..9c37d5f --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/scripts/tunnel.ts @@ -0,0 +1,195 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; +import { DEV_PORT } from '../port'; + +const cwd = process.cwd(); +const envPath = resolve(cwd, '.env'); +const vitePath = resolve(cwd, 'vite.config.ts'); + +let tunnelUrl: string | null = null; +let statusBarActive = false; + +// ── ANSI status bar ────────────────────────────────────────────── +// Reserves the bottom row of the terminal for a persistent status line. +// Logs scroll in the region above it. + +function getColumns(): number { + return process.stdout.columns || 80; +} + +function getRows(): number { + return process.stdout.rows || 24; +} + +function setupScrollRegion(): void { + if (!process.stdout.isTTY) return; + statusBarActive = true; + const rows = getRows(); + // Set scroll region to all rows except the last + process.stdout.write(`\x1b[1;${rows - 1}r`); + // Move cursor into scroll region + process.stdout.write(`\x1b[${rows - 1};1H`); +} + +function drawStatusBar(text: string): void { + if (!process.stdout.isTTY) { + process.stdout.write(text + '\n'); + return; + } + const rows = getRows(); + const cols = getColumns(); + // Save cursor, move to bottom row, clear it, write status, restore cursor + process.stdout.write('\x1b7'); + process.stdout.write(`\x1b[${rows};1H`); + process.stdout.write('\x1b[2K'); + // Inverse video for the bar + process.stdout.write(`\x1b[7m ${text.padEnd(cols - 1)}\x1b[0m`); + process.stdout.write('\x1b8'); +} + +function clearStatusBar(): void { + if (!process.stdout.isTTY || !statusBarActive) return; + const rows = getRows(); + // Reset scroll region to full terminal + process.stdout.write(`\x1b[1;${rows}r`); + // Clear the bottom row + process.stdout.write(`\x1b[${rows};1H\x1b[2K`); + // Move cursor up + process.stdout.write(`\x1b[${rows - 1};1H`); + statusBarActive = false; +} + +function writeLog(text: string): void { + if (statusBarActive && process.stdout.isTTY) { + // Write inside the scroll region, which auto-scrolls above the bar + process.stdout.write(text); + } else { + process.stdout.write(text); + } +} + +// Redraw on terminal resize +process.stdout.on('resize', () => { + if (!statusBarActive || !tunnelUrl) return; + setupScrollRegion(); + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); +}); + +// ── .env helpers ───────────────────────────────────────────────── + +function readEnv(): string { + return readFileSync(envPath, 'utf8'); +} + +function writeEnv(content: string): void { + writeFileSync(envPath, content); +} + +function setEnvVar(key: string, value: string): void { + let env = readEnv(); + const re = new RegExp(`^(#\\s*)?${key}=.*$`, 'm'); + const line = `${key}=${value}`; + + if (re.test(env)) { + env = env.replace(re, line); + } else { + env = env.trimEnd() + '\n' + line + '\n'; + } + writeEnv(env); +} + +function clearEnvVar(key: string): void { + let env = readEnv(); + const re = new RegExp(`^${key}=.*$`, 'm'); + + if (re.test(env)) { + env = env.replace(re, `# ${key}=`); + writeEnv(env); + } +} + +// ── vite config helpers ────────────────────────────────────────── + +function setViteAllowedHosts(hostname: string): void { + let vite = readFileSync(vitePath, 'utf8'); + + if (/allowedHosts\s*:/.test(vite)) { + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, `allowedHosts: ['${hostname}']`); + } else if (/server\s*:\s*\{/.test(vite)) { + vite = vite.replace(/server\s*:\s*\{/, `server: {\n\t\tallowedHosts: ['${hostname}'],`); + } + + writeFileSync(vitePath, vite); +} + +function clearViteAllowedHosts(): void { + let vite = readFileSync(vitePath, 'utf8'); + + if (/allowedHosts\s*:/.test(vite)) { + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, 'allowedHosts: []'); + } + + writeFileSync(vitePath, vite); +} + +// ── cleanup ────────────────────────────────────────────────────── + +function cleanup(): void { + clearStatusBar(); + console.log('\nCleaning up...'); + if (tunnelUrl) { + clearEnvVar('OAUTH_PUBLIC_URL'); + console.log(' Cleared OAUTH_PUBLIC_URL from .env'); + clearViteAllowedHosts(); + console.log(' Cleared allowedHosts from vite.config.ts'); + } +} + +// ── main ───────────────────────────────────────────────────────── + +const child = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${DEV_PORT}`], { + stdio: ['ignore', 'pipe', 'pipe'] +}); + +child.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + + if (!tunnelUrl) { + const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); + if (match) { + tunnelUrl = match[0]; + const hostname = new URL(tunnelUrl).hostname; + + setEnvVar('OAUTH_PUBLIC_URL', tunnelUrl); + setViteAllowedHosts(hostname); + + writeLog(`\n Set OAUTH_PUBLIC_URL=${tunnelUrl}\n`); + writeLog(` Set vite allowedHosts to [${hostname}]\n`); + writeLog(` Tunnel is ready! Restart your dev server to pick up the new URL.\n\n`); + + setupScrollRegion(); + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); + return; + } + } + + writeLog(output); +}); + +child.stdout.on('data', (data: Buffer) => { + writeLog(data.toString()); +}); + +child.on('close', (code) => { + cleanup(); + process.exit(code ?? 0); +}); + +process.on('SIGINT', () => { + child.kill('SIGINT'); +}); + +process.on('SIGTERM', () => { + child.kill('SIGTERM'); +}); diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/kv-store.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/kv-store.ts new file mode 100644 index 0000000..35c3dcb --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/kv-store.ts @@ -0,0 +1,38 @@ +import type { Store } from '@atcute/oauth-node-client'; + +export class KVStore implements Store { + private kv: KVNamespace; + private expirationTtl?: number; + + constructor(kv: KVNamespace, options?: { expirationTtl?: number }) { + this.kv = kv; + this.expirationTtl = options?.expirationTtl; + } + + async get(key: K): Promise { + const value = await this.kv.get(key, 'text'); + if (value === null) return undefined; + return JSON.parse(value) as V; + } + + async set(key: K, value: V): Promise { + await this.kv.put(key, JSON.stringify(value), { + expirationTtl: this.expirationTtl + }); + } + + async delete(key: K): Promise { + await this.kv.delete(key); + } + + async clear(): Promise { + let cursor: string | undefined; + do { + const result = await this.kv.list({ cursor }); + for (const key of result.keys) { + await this.kv.delete(key.name); + } + cursor = result.list_complete ? undefined : result.cursor; + } while (cursor); + } +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.remote.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.remote.ts new file mode 100644 index 0000000..f087d53 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.remote.ts @@ -0,0 +1,56 @@ +import * as v from 'valibot'; +import { error } from '@sveltejs/kit'; +import { command, getRequestEvent } from '$app/server'; +import { createOAuthClient } from './oauth'; +import { getSignedCookie } from './signed-cookie'; +import { scopes, signUpPDS } from '../settings'; +import type { ActorIdentifier, Did } from '@atcute/lexicons'; + +export const oauthLogin = command( + v.object({ + handle: v.optional(v.pipe(v.string(), v.minLength(3))), + signup: v.optional(v.boolean()) + }), + async (input) => { + const { platform } = getRequestEvent(); + + try { + const oauth = createOAuthClient(platform?.env); + + const target = input.signup + ? ({ type: 'pds', serviceUrl: signUpPDS } as const) + : ({ type: 'account', identifier: input.handle as ActorIdentifier } as const); + + const { url } = await oauth.authorize({ + target, + scope: scopes.join(' '), + prompt: input.signup ? 'create' : undefined + }); + + return { url: url.toString() }; + } catch (e) { + if (e && typeof e === 'object' && 'status' in e) throw e; // re-throw SvelteKit errors + const message = e instanceof Error ? e.message : 'Login failed'; + error(400, message); + } + } +); + +export const oauthLogout = command(async () => { + const { cookies, platform } = getRequestEvent(); + const did = getSignedCookie(cookies, 'did') as Did | null; + + if (did) { + try { + const oauth = createOAuthClient(platform?.env); + await oauth.revoke(did); + } catch (e) { + console.error('Error revoking session:', e); + } + } + + cookies.delete('did', { path: '/' }); + cookies.delete('scope', { path: '/' }); + + return { ok: true }; +}); diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.ts new file mode 100644 index 0000000..e0d0f95 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.ts @@ -0,0 +1,97 @@ +import { + OAuthClient, + MemoryStore, + type ClientAssertionPrivateJwk, + type OAuthClientStores, + type OAuthSession, + type StoredSession, + type StoredState +} from '@atcute/oauth-node-client'; +import type { Did } from '@atcute/lexicons'; +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + LocalActorResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver +} from '@atcute/identity-resolver'; +import { KVStore } from './kv-store'; +import { DOH_RESOLVER, REDIRECT_PATH, scopes } from '../settings'; +import { DEV_PORT } from '../port'; +import { dev } from '$app/environment'; + +function createActorResolver() { + return new LocalActorResolver({ + handleResolver: new CompositeHandleResolver({ + methods: { + dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), + http: new WellKnownHandleResolver() + } + }), + didDocumentResolver: new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver() + } + }) + }); +} + +function createStores(env?: App.Platform['env']): OAuthClientStores { + if (env?.OAUTH_SESSIONS && env?.OAUTH_STATES) { + return { + sessions: new KVStore(env.OAUTH_SESSIONS), + states: new KVStore(env.OAUTH_STATES, { expirationTtl: 600 }) + }; + } + // Fallback to in-memory stores (dev without wrangler) + return { + sessions: new MemoryStore(), + states: new MemoryStore({ ttl: 600_000 }) + }; +} + +export function createOAuthClient(env?: App.Platform['env']): OAuthClient { + const actorResolver = createActorResolver(); + const stores = createStores(env); + + if (dev && !env?.OAUTH_PUBLIC_URL) { + // Dev without tunnel: loopback public client (no keyset). + // Omit client_id — the library builds it automatically from redirect_uris + scope. + // redirect_uris must use 127.0.0.1 (not localhost). + return new OAuthClient({ + metadata: { + redirect_uris: [`http://127.0.0.1:${DEV_PORT}${REDIRECT_PATH}`], + scope: scopes + }, + actorResolver, + stores + }); + } + + // Confidential client (production, or dev with tunnel via OAUTH_PUBLIC_URL) + if (!env?.OAUTH_PUBLIC_URL) { + throw new Error('OAUTH_PUBLIC_URL is not set'); + } + if (!env.CLIENT_ASSERTION_KEY) { + throw new Error('CLIENT_ASSERTION_KEY secret is not set. Run: pnpm env:generate-key'); + } + const site = env.OAUTH_PUBLIC_URL; + const key: ClientAssertionPrivateJwk = JSON.parse(env.CLIENT_ASSERTION_KEY); + + return new OAuthClient({ + metadata: { + client_id: site + '/oauth-client-metadata.json', + redirect_uris: [site + REDIRECT_PATH], + scope: scopes, + jwks_uri: site + '/oauth/jwks.json' + }, + keyset: [key], + actorResolver, + stores + }); +} + +export type { OAuthSession }; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/profile.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/profile.ts new file mode 100644 index 0000000..20521fc --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/profile.ts @@ -0,0 +1,51 @@ +import type { Did } from '@atcute/lexicons'; +import { getDetailedProfile, describeRepo } from '../methods'; + +const PROFILE_CACHE_TTL = 60 * 60; // 1 hour + +/** + * Loads a user's profile, with optional KV caching. + * Falls back to a fresh fetch if the cache KV doesn't exist or on cache miss. + * Returns undefined if the profile can't be loaded. + */ +export async function loadProfile(did: Did, profileCache?: KVNamespace) { + // Try cache first + if (profileCache) { + try { + const cached = await profileCache.get(did, 'json'); + if (cached) return cached as Record; + } catch { + // Cache read failed, continue to fresh fetch + } + } + + const profile = await fetchProfile(did); + + // Write to cache (fire-and-forget) + if (profileCache && profile) { + profileCache + .put(did, JSON.stringify(profile), { expirationTtl: PROFILE_CACHE_TTL }) + .catch(() => {}); + } + + return profile; +} + +async function fetchProfile(did: Did) { + try { + let profile = await getDetailedProfile({ did }); + + if (!profile || profile.handle === 'handle.invalid') { + const repo = await describeRepo({ did }); + profile = { + did, + handle: repo?.handle || 'handle.invalid' + } as typeof profile; + } + + return profile; + } catch (e) { + console.error('Failed to load profile:', e); + return undefined; + } +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts new file mode 100644 index 0000000..a9db6fb --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts @@ -0,0 +1,94 @@ +import { error } from '@sveltejs/kit'; +import { command, getRequestEvent } from '$app/server'; +import * as v from 'valibot'; +import { collections } from '../settings'; +import { contrail, ensureInit } from '$lib/contrail'; + +// Validate collection format and check against allowed list from settings +const collectionSchema = v.pipe( + v.string(), + v.regex(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*){2,}$/), + v.check((c) => collections.includes(c as (typeof collections)[number]), 'Collection not in allowed list') +); + +// AT Protocol rkey: TID, 'self', or other valid record keys (alphanumeric, dash, underscore, dot) +const rkeySchema = v.optional(v.pipe(v.string(), v.regex(/^[a-zA-Z0-9._:~-]{1,512}$/))); + +export const putRecord = command( + v.object({ + collection: collectionSchema, + rkey: rkeySchema, + record: v.record(v.string(), v.unknown()) + }), + async (input) => { + const { locals } = getRequestEvent(); + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + + const response = await locals.client.post('com.atproto.repo.putRecord', { + input: { + collection: input.collection as `${string}.${string}.${string}`, + repo: locals.did, + rkey: input.rkey || 'self', + record: input.record + } + }); + + // Immediately index the new/updated record in contrail + const { platform } = getRequestEvent(); + const db = platform?.env?.DB; + if (db) { + await ensureInit(db); + await contrail.notify(response.data.uri, db).catch(() => {}); + } + + return response.data; + } +); + +export const deleteRecord = command( + v.object({ + collection: collectionSchema, + rkey: rkeySchema + }), + async (input) => { + const { locals } = getRequestEvent(); + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + + const response = await locals.client.post('com.atproto.repo.deleteRecord', { + input: { + collection: input.collection as `${string}.${string}.${string}`, + repo: locals.did, + rkey: input.rkey || 'self' + } + }); + + return { ok: response.ok }; + } +); + +export const uploadBlob = command( + v.object({ + bytes: v.array(v.number()), + mimeType: v.string() + }), + async (input) => { + const { locals } = getRequestEvent(); + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + + const blob = new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }); + + const response = await locals.client.post('com.atproto.repo.uploadBlob', { + params: { repo: locals.did }, + input: blob + }); + + if (!response.ok) error(500, 'Upload failed'); + + return response.data.blob as { + $type: 'blob'; + ref: { $link: string }; + mimeType: string; + size: number; + }; + } +); diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/session.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/session.ts new file mode 100644 index 0000000..2c1fe8f --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/session.ts @@ -0,0 +1,73 @@ +import type { Cookies } from '@sveltejs/kit'; +import { Client } from '@atcute/client'; +import type { Did } from '@atcute/lexicons'; +import { + type OAuthSession, + TokenInvalidError, + TokenRefreshError, + TokenRevokedError, + AuthMethodUnsatisfiableError +} from '@atcute/oauth-node-client'; +import { createOAuthClient } from './oauth'; +import { getSignedCookie } from './signed-cookie'; +import { scopes } from '../settings'; + +export type SessionLocals = { + session: OAuthSession | null; + client: Client | null; + did: Did | null; +}; + +/** + * Restores an OAuth session from the signed `did` cookie. + * Returns session locals to be assigned to `event.locals`. + * Deletes the cookie only if the session is genuinely unrecoverable. + * Transient failures (network, KV) preserve the cookie for retry. + */ +export async function restoreSession( + cookies: Cookies, + env?: App.Platform['env'] +): Promise { + const did = getSignedCookie(cookies, 'did') as Did | null; + + if (!did) { + return { session: null, client: null, did: null }; + } + + // If permissions changed since login, invalidate the session + const savedScope = getSignedCookie(cookies, 'scope'); + if (savedScope !== null && savedScope !== scopes.join(' ')) { + cookies.delete('did', { path: '/' }); + cookies.delete('scope', { path: '/' }); + return { session: null, client: null, did: null }; + } + + try { + const oauth = createOAuthClient(env); + const session = await oauth.restore(did); + + return { + session, + client: new Client({ handler: session }), + did + }; + } catch (e) { + console.error('Failed to restore session:', e); + + // Only delete cookies when the session is genuinely unrecoverable. + // Transient errors (network issues, KV hiccups) should preserve the + // cookie so the next request can retry without forcing a full re-login. + const isSessionGone = + e instanceof TokenInvalidError || + e instanceof TokenRevokedError || + e instanceof TokenRefreshError || + e instanceof AuthMethodUnsatisfiableError; + + if (isSessionGone) { + cookies.delete('did', { path: '/' }); + cookies.delete('scope', { path: '/' }); + } + + return { session: null, client: null, did: null }; + } +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts new file mode 100644 index 0000000..6cde19c --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts @@ -0,0 +1,69 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; + +import type { Cookies } from '@sveltejs/kit'; + +import { env } from '$env/dynamic/private'; +import { dev } from '$app/environment'; + +const SEPARATOR = '.'; + +function getSecret(): string { + const secret = env.COOKIE_SECRET; + if (secret) return secret; + if (dev) return 'dev-cookie-secret-not-for-production'; + throw new Error('COOKIE_SECRET is not set'); +} + +function toBase64Url(bytes: Uint8Array): string { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function fromBase64Url(str: string): Uint8Array { + const padded = str + '='.repeat((4 - (str.length % 4)) % 4); + const base64 = padded.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +function hmacSha256(data: string): Uint8Array { + return createHmac('sha256', getSecret()).update(data).digest(); +} + +export function getSignedCookie(cookies: Cookies, name: string): string | null { + const signed = cookies.get(name); + if (!signed) return null; + + const idx = signed.lastIndexOf(SEPARATOR); + if (idx === -1) return null; + + const value = signed.slice(0, idx); + const sig = signed.slice(idx + 1); + + let expected: Uint8Array; + let got: Uint8Array; + try { + expected = hmacSha256(value); + got = fromBase64Url(sig); + } catch { + return null; + } + + if (got.length !== expected.length || !timingSafeEqual(got, expected)) return null; + + return value; +} + +export function setSignedCookie( + cookies: Cookies, + name: string, + value: string, + options: Parameters[2] +): void { + const sig = toBase64Url(hmacSha256(value)); + const signed = `${value}${SEPARATOR}${sig}`; + cookies.set(name, signed, options); +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/settings.ts b/examples/sveltekit-group-chat/src/lib/atproto/settings.ts new file mode 100644 index 0000000..b187107 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/settings.ts @@ -0,0 +1,30 @@ +import { dev } from '$app/environment'; +import { scope } from '@atcute/oauth-node-client'; + +// writable collections +export const collections = ['xyz.statusphere.status', 'social.atmo.test.blob'] as const; + +export type AllowedCollection = (typeof collections)[number]; + +// OAuth scope — add scope.blob(), scope.rpc(), etc. as needed +export const scopes = [ + 'atproto', + scope.repo({ collection: [...collections] }), + scope.blob({ accept: ['image/*'] }) +]; + +// set to false to disable signup +export const ALLOW_SIGNUP = true; + +// which PDS to use for signup (change to your preferred PDS) +const devPDS = 'https://pds.rip/'; +const prodPDS = 'https://selfhosted.social/'; +export const signUpPDS = dev ? devPDS : prodPDS; + +// where to redirect after oauth login/signup +export const REDIRECT_PATH = '/oauth/callback'; + +// redirect the user back to the page they were on before login +export const REDIRECT_TO_LAST_PAGE_ON_LOGIN = true; + +export const DOH_RESOLVER = 'https://mozilla.cloudflare-dns.com/dns-query'; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/ui/LoginModal.svelte b/examples/sveltekit-group-chat/src/lib/atproto/ui/LoginModal.svelte new file mode 100644 index 0000000..b3bad3a --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/ui/LoginModal.svelte @@ -0,0 +1,22 @@ + + + + + { + await login(handle); + return true; + }} + signup={async () => { + signup(); + return true; + }} +/> diff --git a/examples/sveltekit-group-chat/src/lib/contrail/client.ts b/examples/sveltekit-group-chat/src/lib/contrail/client.ts new file mode 100644 index 0000000..f9d7dc9 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/contrail/client.ts @@ -0,0 +1,32 @@ +import { Client, simpleFetchHandler } from '@atcute/client'; + +export interface Profile { + handle: string; + displayName?: string; + avatar?: string; +} + +/** + * Extract a simple profile from a contrail profile entry. + * Contrail returns { did, handle, record: { displayName, avatar, ... } } + * while components expect { handle, displayName?, avatar? }. + */ +export function extractProfile(entry: { + did: string; + handle?: string; + record?: unknown; +}): Profile { + const record = entry.record as { displayName?: string; avatar?: string } | undefined; + return { + handle: entry.handle ?? entry.did, + displayName: record?.displayName, + avatar: record?.avatar + }; +} + +/** + * Client-side: fully typed @atcute/client that queries the app's own /xrpc/ endpoints. + */ +export function getClient() { + return new Client({ handler: simpleFetchHandler({ service: '' }) }); +} diff --git a/examples/sveltekit-group-chat/src/lib/contrail/config.ts b/examples/sveltekit-group-chat/src/lib/contrail/config.ts new file mode 100644 index 0000000..b3aa09a --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/contrail/config.ts @@ -0,0 +1,14 @@ +import type { ContrailConfig } from '@atmo-dev/contrail'; + +export const config: ContrailConfig = { + namespace: 'statusphere.app', + collections: { + status: { + collection: 'xyz.statusphere.status', + queryable: { + status: {}, + createdAt: { type: 'range' } + } + } + } +}; diff --git a/examples/sveltekit-group-chat/src/lib/contrail/index.ts b/examples/sveltekit-group-chat/src/lib/contrail/index.ts new file mode 100644 index 0000000..aa8c784 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/contrail/index.ts @@ -0,0 +1,31 @@ +import { Contrail } from '@atmo-dev/contrail'; +import { createHandler } from '@atmo-dev/contrail/server'; +import { Client } from '@atcute/client'; +import { config } from './config'; + +export const contrail = new Contrail(config); + +let initialized = false; + +export async function ensureInit(db: D1Database) { + if (!initialized) { + await contrail.init(db); + initialized = true; + } +} + +const handle = createHandler(contrail); + +/** + * Server-side: fully typed @atcute/client that routes through contrail in-process. + * No HTTP roundtrip — calls createHandler directly. + */ +export function getServerClient(db: D1Database) { + return new Client({ + handler: async (pathname, init) => { + await ensureInit(db); + const url = new URL(pathname, 'http://localhost'); + return handle(new Request(url, init), db) as Promise; + } + }); +} diff --git a/examples/sveltekit-group-chat/src/lib/emojis/icons.json b/examples/sveltekit-group-chat/src/lib/emojis/icons.json new file mode 100644 index 0000000..6266e50 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/emojis/icons.json @@ -0,0 +1,7255 @@ +{ + "host": "", + "asset_url_pattern": "", + "families": ["Animated Emoji"], + "icons": [ + { + "name": "emoji_u1f600", + "version": 1, + "popularity": 720, + "codepoint": "1f600", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f603", + "version": 1, + "popularity": 719, + "codepoint": "1f603", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smile-with-big-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f604", + "version": 1, + "popularity": 718, + "codepoint": "1f604", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f601", + "version": 1, + "popularity": 717, + "codepoint": "1f601", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grinning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f606", + "version": 1, + "popularity": 716, + "codepoint": "1f606", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":laughing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f605", + "version": 1, + "popularity": 715, + "codepoint": "1f605", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grin-sweat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f602", + "version": 1, + "popularity": 714, + "codepoint": "1f602", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":joy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f923", + "version": 1, + "popularity": 713, + "codepoint": "1f923", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rofl:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62d", + "version": 1, + "popularity": 712, + "codepoint": "1f62d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":loudly-crying:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f609", + "version": 1, + "popularity": 711, + "codepoint": "1f609", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wink:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f617", + "version": 1, + "popularity": 710, + "codepoint": "1f617", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f619", + "version": 1, + "popularity": 709, + "codepoint": "1f619", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-smiling-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61a", + "version": 1, + "popularity": 708, + "codepoint": "1f61a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-closed-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f618", + "version": 1, + "popularity": 707, + "codepoint": "1f618", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f970", + "version": 1, + "popularity": 706, + "codepoint": "1f970", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-face:", ":3-hearts:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60d", + "version": 1, + "popularity": 705, + "codepoint": "1f60d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f929", + "version": 1, + "popularity": 704, + "codepoint": "1f929", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":star-struck:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f973", + "version": 1, + "popularity": 703, + "codepoint": "1f973", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":partying-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae0", + "version": 1, + "popularity": 702, + "codepoint": "1fae0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":melting:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f643", + "version": 1, + "popularity": 701, + "codepoint": "1f643", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":upside-down-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f642", + "version": 1, + "popularity": 700, + "codepoint": "1f642", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":slightly-happy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f972", + "version": 1, + "popularity": 699, + "codepoint": "1f972", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":happy-cry:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f979", + "version": 1, + "popularity": 698, + "codepoint": "1f979", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":holding-back-tears:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60a", + "version": 1, + "popularity": 697, + "codepoint": "1f60a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":blush:"], + "sizes_px": [] + }, + { + "name": "emoji_u263a_fe0f", + "version": 1, + "popularity": 696, + "codepoint": "263a_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":warm-smile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60c", + "version": 1, + "popularity": 695, + "codepoint": "1f60c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":relieved:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f642_200d_2195_fe0f", + "version": 1, + "popularity": 694, + "codepoint": "1f642_200d_2195_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":head-nod:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f642_200d_2194_fe0f", + "version": 1, + "popularity": 693, + "codepoint": "1f642_200d_2194_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":head-shake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60f", + "version": 1, + "popularity": 692, + "codepoint": "1f60f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smirk:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f924", + "version": 1, + "popularity": 691, + "codepoint": "1f924", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":drool:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60b", + "version": 1, + "popularity": 690, + "codepoint": "1f60b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":yum:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61b", + "version": 1, + "popularity": 689, + "codepoint": "1f61b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":stuck-out-tongue:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61d", + "version": 1, + "popularity": 688, + "codepoint": "1f61d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":squinting-tongue:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61c", + "version": 1, + "popularity": 687, + "codepoint": "1f61c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":winky-tongue:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92a", + "version": 1, + "popularity": 686, + "codepoint": "1f92a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":zany-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f974", + "version": 1, + "popularity": 685, + "codepoint": "1f974", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":woozy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f614", + "version": 1, + "popularity": 684, + "codepoint": "1f614", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pensive:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f97a", + "version": 1, + "popularity": 683, + "codepoint": "1f97a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pleading:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62c", + "version": 1, + "popularity": 682, + "codepoint": "1f62c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grimacing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f611", + "version": 1, + "popularity": 681, + "codepoint": "1f611", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":expressionless:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f610", + "version": 1, + "popularity": 680, + "codepoint": "1f610", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":neutral-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f636", + "version": 1, + "popularity": 679, + "codepoint": "1f636", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mouth-none:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f636_200d_1f32b_fe0f", + "version": 1, + "popularity": 678, + "codepoint": "1f636_200d_1f32b_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":face-in-clouds:", ":lost:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae5", + "version": 1, + "popularity": 677, + "codepoint": "1fae5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":dotted-line-face:", ":invisible:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f910", + "version": 1, + "popularity": 676, + "codepoint": "1f910", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":zipper-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae1", + "version": 1, + "popularity": 675, + "codepoint": "1fae1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":salute:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f914", + "version": 1, + "popularity": 674, + "codepoint": "1f914", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thinking-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92b", + "version": 1, + "popularity": 673, + "codepoint": "1f92b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":shushing-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae2", + "version": 1, + "popularity": 672, + "codepoint": "1fae2", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-over-mouth:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92d", + "version": 1, + "popularity": 671, + "codepoint": "1f92d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smiling-eyes-with-hand-over-mouth:", ":chuckling:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f971", + "version": 1, + "popularity": 670, + "codepoint": "1f971", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":yawn:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f917", + "version": 1, + "popularity": 669, + "codepoint": "1f917", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hug-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae3", + "version": 1, + "popularity": 668, + "codepoint": "1fae3", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":peeking:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f631", + "version": 1, + "popularity": 667, + "codepoint": "1f631", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":screaming:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f928", + "version": 1, + "popularity": 666, + "codepoint": "1f928", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-eyebrow:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9d0", + "version": 1, + "popularity": 665, + "codepoint": "1f9d0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":monocle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f612", + "version": 1, + "popularity": 664, + "codepoint": "1f612", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":unamused:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f644", + "version": 1, + "popularity": 663, + "codepoint": "1f644", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rolling-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62e_200d_1f4a8", + "version": 1, + "popularity": 662, + "codepoint": "1f62e_200d_1f4a8", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":exhale:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f624", + "version": 1, + "popularity": 661, + "codepoint": "1f624", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":triumph:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f620", + "version": 1, + "popularity": 660, + "codepoint": "1f620", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":angry:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f621", + "version": 1, + "popularity": 659, + "codepoint": "1f621", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rage:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92c", + "version": 1, + "popularity": 658, + "codepoint": "1f92c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cursing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61e", + "version": 1, + "popularity": 657, + "codepoint": "1f61e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sad:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f613", + "version": 1, + "popularity": 656, + "codepoint": "1f613", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sweat:", ":downcast:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61f", + "version": 1, + "popularity": 655, + "codepoint": "1f61f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":worried:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f625", + "version": 1, + "popularity": 654, + "codepoint": "1f625", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":concerned:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f622", + "version": 1, + "popularity": 653, + "codepoint": "1f622", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cry:"], + "sizes_px": [] + }, + { + "name": "emoji_u2639_fe0f", + "version": 1, + "popularity": 652, + "codepoint": "2639_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":big-frown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f641", + "version": 1, + "popularity": 651, + "codepoint": "1f641", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":frown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae4", + "version": 1, + "popularity": 650, + "codepoint": "1fae4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":diagonal-mouth:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f615", + "version": 1, + "popularity": 649, + "codepoint": "1f615", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":slightly-frowning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f630", + "version": 1, + "popularity": 648, + "codepoint": "1f630", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":anxious-with-sweat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f628", + "version": 1, + "popularity": 647, + "codepoint": "1f628", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scared:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f627", + "version": 1, + "popularity": 646, + "codepoint": "1f627", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":anguished:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f626", + "version": 1, + "popularity": 645, + "codepoint": "1f626", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":gasp:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62e", + "version": 1, + "popularity": 644, + "codepoint": "1f62e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mouth-open:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62f", + "version": 1, + "popularity": 643, + "codepoint": "1f62f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":surprised:", ":hushed:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f632", + "version": 1, + "popularity": 642, + "codepoint": "1f632", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":astonished:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f633", + "version": 1, + "popularity": 641, + "codepoint": "1f633", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":flushed:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92f", + "version": 1, + "popularity": 640, + "codepoint": "1f92f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mind-blown:", ":exploding-head:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f616", + "version": 1, + "popularity": 639, + "codepoint": "1f616", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scrunched-mouth:", ":confounded:", ":zigzag-mouth:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f623", + "version": 1, + "popularity": 638, + "codepoint": "1f623", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scrunched-eyes:", ":persevering:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f629", + "version": 1, + "popularity": 637, + "codepoint": "1f629", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":weary:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62b", + "version": 1, + "popularity": 636, + "codepoint": "1f62b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":distraught:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f635", + "version": 1, + "popularity": 635, + "codepoint": "1f635", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":x-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f635_200d_1f4ab", + "version": 1, + "popularity": 634, + "codepoint": "1f635_200d_1f4ab", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":dizzy-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae8", + "version": 1, + "popularity": 633, + "codepoint": "1fae8", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":shaking-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f976", + "version": 1, + "popularity": 632, + "codepoint": "1f976", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cold-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f975", + "version": 1, + "popularity": 631, + "codepoint": "1f975", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hot-face:", ":sweat-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f922", + "version": 1, + "popularity": 630, + "codepoint": "1f922", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sick:", ":nauseated:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92e", + "version": 1, + "popularity": 629, + "codepoint": "1f92e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vomit:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae9", + "version": 1, + "popularity": 628, + "codepoint": "1fae9", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":tired:", ":bags-under-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f634", + "version": 1, + "popularity": 627, + "codepoint": "1f634", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sleep:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62a", + "version": 1, + "popularity": 626, + "codepoint": "1f62a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sleepy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f927", + "version": 1, + "popularity": 625, + "codepoint": "1f927", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sneeze:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f912", + "version": 1, + "popularity": 624, + "codepoint": "1f912", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thermometer-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f915", + "version": 1, + "popularity": 623, + "codepoint": "1f915", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":bandage-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f637", + "version": 1, + "popularity": 622, + "codepoint": "1f637", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mask:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f925", + "version": 1, + "popularity": 621, + "codepoint": "1f925", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":liar:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f607", + "version": 1, + "popularity": 620, + "codepoint": "1f607", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":halo:", ":innocent:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f920", + "version": 1, + "popularity": 619, + "codepoint": "1f920", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cowboy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f911", + "version": 1, + "popularity": 618, + "codepoint": "1f911", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":money-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f913", + "version": 1, + "popularity": 617, + "codepoint": "1f913", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nerd-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60e", + "version": 1, + "popularity": 616, + "codepoint": "1f60e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sunglasses-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f978", + "version": 1, + "popularity": 615, + "codepoint": "1f978", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":disguise:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f921", + "version": 1, + "popularity": 614, + "codepoint": "1f921", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a9", + "version": 1, + "popularity": 613, + "codepoint": "1f4a9", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":poop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f608", + "version": 1, + "popularity": 612, + "codepoint": "1f608", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":imp-smile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47f", + "version": 1, + "popularity": 611, + "codepoint": "1f47f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":imp-frown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47b", + "version": 1, + "popularity": 610, + "codepoint": "1f47b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ghost:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f480", + "version": 1, + "popularity": 609, + "codepoint": "1f480", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":skull:"], + "sizes_px": [] + }, + { + "name": "emoji_u2603_fe0f", + "version": 1, + "popularity": 608, + "codepoint": "2603_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":snowman-with-snow:"], + "sizes_px": [] + }, + { + "name": "emoji_u26c4", + "version": 1, + "popularity": 607, + "codepoint": "26c4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":snowman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f383", + "version": 1, + "popularity": 606, + "codepoint": "1f383", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":jack-o-lantern:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f916", + "version": 1, + "popularity": 605, + "codepoint": "1f916", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":robot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47d", + "version": 1, + "popularity": 604, + "codepoint": "1f47d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":alien:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47e", + "version": 1, + "popularity": 603, + "codepoint": "1f47e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":alien-monster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31e", + "version": 1, + "popularity": 602, + "codepoint": "1f31e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sun-with-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31b", + "version": 1, + "popularity": 601, + "codepoint": "1f31b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":moon-face-first-quarter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31c", + "version": 1, + "popularity": 600, + "codepoint": "1f31c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":moon-face-last-quarter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63a", + "version": 1, + "popularity": 599, + "codepoint": "1f63a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smiley-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f638", + "version": 1, + "popularity": 598, + "codepoint": "1f638", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smile-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f639", + "version": 1, + "popularity": 597, + "codepoint": "1f639", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":joy-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63b", + "version": 1, + "popularity": 596, + "codepoint": "1f63b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-eyes-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63c", + "version": 1, + "popularity": 595, + "codepoint": "1f63c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smirk-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63d", + "version": 1, + "popularity": 594, + "codepoint": "1f63d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f640", + "version": 1, + "popularity": 593, + "codepoint": "1f640", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scream-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63f", + "version": 1, + "popularity": 592, + "codepoint": "1f63f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crying-cat-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63e", + "version": 1, + "popularity": 591, + "codepoint": "1f63e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pouting-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f648", + "version": 1, + "popularity": 590, + "codepoint": "1f648", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":see-no-evil-monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f649", + "version": 1, + "popularity": 589, + "codepoint": "1f649", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hear-no-evil-monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64a", + "version": 1, + "popularity": 588, + "codepoint": "1f64a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":speak-no-evil-monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31f", + "version": 1, + "popularity": 587, + "codepoint": "1f31f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":glowing-star:"], + "sizes_px": [] + }, + { + "name": "emoji_u2728", + "version": 1, + "popularity": 586, + "codepoint": "2728", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sparkles:"], + "sizes_px": [] + }, + { + "name": "emoji_u26a1", + "version": 1, + "popularity": 585, + "codepoint": "26a1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":electricity:", ":zap:", ":lightning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a5", + "version": 1, + "popularity": 584, + "codepoint": "1f4a5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":collision:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f525", + "version": 1, + "popularity": 583, + "codepoint": "1f525", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fire:", ":burn:", ":lit:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4af", + "version": 1, + "popularity": 582, + "codepoint": "1f4af", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":100:", ":one-hundred:", ":hundred:", ":points:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f389", + "version": 1, + "popularity": 581, + "codepoint": "1f389", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":party-popper:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f38a", + "version": 1, + "popularity": 580, + "codepoint": "1f38a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":confetti-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u2764_fe0f", + "version": 1, + "popularity": 579, + "codepoint": "2764_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":red-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9e1", + "version": 1, + "popularity": 578, + "codepoint": "1f9e1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":orange-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49b", + "version": 1, + "popularity": 577, + "codepoint": "1f49b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":yellow-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49a", + "version": 1, + "popularity": 576, + "codepoint": "1f49a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":green-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa75", + "version": 1, + "popularity": 575, + "codepoint": "1fa75", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":light-blue-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f499", + "version": 1, + "popularity": 574, + "codepoint": "1f499", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":blue-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49c", + "version": 1, + "popularity": 573, + "codepoint": "1f49c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":purple-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90e", + "version": 1, + "popularity": 572, + "codepoint": "1f90e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":brown-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f5a4", + "version": 1, + "popularity": 571, + "codepoint": "1f5a4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":black-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa76", + "version": 1, + "popularity": 570, + "codepoint": "1fa76", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grey-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90d", + "version": 1, + "popularity": 569, + "codepoint": "1f90d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":white-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa77", + "version": 1, + "popularity": 568, + "codepoint": "1fa77", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pink-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f498", + "version": 1, + "popularity": 567, + "codepoint": "1f498", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cupid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49d", + "version": 1, + "popularity": 566, + "codepoint": "1f49d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":gift-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f496", + "version": 1, + "popularity": 565, + "codepoint": "1f496", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sparkling-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f497", + "version": 1, + "popularity": 564, + "codepoint": "1f497", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-grow:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f493", + "version": 1, + "popularity": 563, + "codepoint": "1f493", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":beating-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49e", + "version": 1, + "popularity": 562, + "codepoint": "1f49e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":revolving-hearts:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f495", + "version": 1, + "popularity": 561, + "codepoint": "1f495", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":two-hearts:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48c", + "version": 1, + "popularity": 560, + "codepoint": "1f48c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-letter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49f", + "version": 1, + "popularity": 559, + "codepoint": "1f49f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-box:"], + "sizes_px": [] + }, + { + "name": "emoji_u2763_fe0f", + "version": 1, + "popularity": 558, + "codepoint": "2763_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-exclamation-point:"], + "sizes_px": [] + }, + { + "name": "emoji_u2764_fe0f_200d_1fa79", + "version": 1, + "popularity": 557, + "codepoint": "2764_fe0f_200d_1fa79", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":bandaged-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f494", + "version": 1, + "popularity": 556, + "codepoint": "1f494", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":broken-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u2764_fe0f_200d_1f525", + "version": 1, + "popularity": 555, + "codepoint": "2764_fe0f_200d_1f525", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fire-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48b", + "version": 1, + "popularity": 554, + "codepoint": "1f48b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kiss:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f463", + "version": 1, + "popularity": 553, + "codepoint": "1f463", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":footprints:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fac6", + "version": 1, + "popularity": 552, + "codepoint": "1fac6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fingerprint:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fac0", + "version": 1, + "popularity": 551, + "codepoint": "1fac0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":anatomical-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa78", + "version": 1, + "popularity": 550, + "codepoint": "1fa78", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":blood:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9a0", + "version": 1, + "popularity": 549, + "codepoint": "1f9a0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":microbe:", ":virus:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f440", + "version": 1, + "popularity": 548, + "codepoint": "1f440", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f441_fe0f", + "version": 1, + "popularity": 547, + "codepoint": "1f441_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":eye:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae6", + "version": 1, + "popularity": 546, + "codepoint": "1fae6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":biting-lip:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443", + "version": 1, + "popularity": 545, + "codepoint": "1f443", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fb", + "version": 1, + "popularity": 544, + "codepoint": "1f443_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fc", + "version": 1, + "popularity": 543, + "codepoint": "1f443_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fd", + "version": 1, + "popularity": 542, + "codepoint": "1f443_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fe", + "version": 1, + "popularity": 541, + "codepoint": "1f443_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3ff", + "version": 1, + "popularity": 540, + "codepoint": "1f443_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442", + "version": 1, + "popularity": 539, + "codepoint": "1f442", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fb", + "version": 1, + "popularity": 538, + "codepoint": "1f442_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fc", + "version": 1, + "popularity": 537, + "codepoint": "1f442_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fd", + "version": 1, + "popularity": 536, + "codepoint": "1f442_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fe", + "version": 1, + "popularity": 535, + "codepoint": "1f442_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3ff", + "version": 1, + "popularity": 534, + "codepoint": "1f442_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb", + "version": 1, + "popularity": 533, + "codepoint": "1f9bb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fb", + "version": 1, + "popularity": 532, + "codepoint": "1f9bb_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fc", + "version": 1, + "popularity": 531, + "codepoint": "1f9bb_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fd", + "version": 1, + "popularity": 530, + "codepoint": "1f9bb_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fe", + "version": 1, + "popularity": 529, + "codepoint": "1f9bb_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3ff", + "version": 1, + "popularity": 528, + "codepoint": "1f9bb_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6", + "version": 1, + "popularity": 527, + "codepoint": "1f9b6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fb", + "version": 1, + "popularity": 526, + "codepoint": "1f9b6_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fc", + "version": 1, + "popularity": 525, + "codepoint": "1f9b6_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fd", + "version": 1, + "popularity": 524, + "codepoint": "1f9b6_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fe", + "version": 1, + "popularity": 523, + "codepoint": "1f9b6_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3ff", + "version": 1, + "popularity": 522, + "codepoint": "1f9b6_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5", + "version": 1, + "popularity": 521, + "codepoint": "1f9b5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fb", + "version": 1, + "popularity": 520, + "codepoint": "1f9b5_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fc", + "version": 1, + "popularity": 519, + "codepoint": "1f9b5_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fd", + "version": 1, + "popularity": 518, + "codepoint": "1f9b5_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fe", + "version": 1, + "popularity": 517, + "codepoint": "1f9b5_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3ff", + "version": 1, + "popularity": 516, + "codepoint": "1f9b5_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bf", + "version": 1, + "popularity": 515, + "codepoint": "1f9bf", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg-mechanical:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9be", + "version": 1, + "popularity": 514, + "codepoint": "1f9be", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":arm-mechanical:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa", + "version": 1, + "popularity": 513, + "codepoint": "1f4aa", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fb", + "version": 1, + "popularity": 512, + "codepoint": "1f4aa_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fc", + "version": 1, + "popularity": 511, + "codepoint": "1f4aa_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fd", + "version": 1, + "popularity": 510, + "codepoint": "1f4aa_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fe", + "version": 1, + "popularity": 509, + "codepoint": "1f4aa_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3ff", + "version": 1, + "popularity": 508, + "codepoint": "1f4aa_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f", + "version": 1, + "popularity": 507, + "codepoint": "1f44f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fb", + "version": 1, + "popularity": 506, + "codepoint": "1f44f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fc", + "version": 1, + "popularity": 505, + "codepoint": "1f44f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fd", + "version": 1, + "popularity": 504, + "codepoint": "1f44f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fe", + "version": 1, + "popularity": 503, + "codepoint": "1f44f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3ff", + "version": 1, + "popularity": 502, + "codepoint": "1f44f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d", + "version": 1, + "popularity": 501, + "codepoint": "1f44d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fb", + "version": 1, + "popularity": 500, + "codepoint": "1f44d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fc", + "version": 1, + "popularity": 499, + "codepoint": "1f44d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fd", + "version": 1, + "popularity": 498, + "codepoint": "1f44d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fe", + "version": 1, + "popularity": 497, + "codepoint": "1f44d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3ff", + "version": 1, + "popularity": 496, + "codepoint": "1f44d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e", + "version": 1, + "popularity": 495, + "codepoint": "1f44e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fb", + "version": 1, + "popularity": 494, + "codepoint": "1f44e_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fc", + "version": 1, + "popularity": 493, + "codepoint": "1f44e_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fd", + "version": 1, + "popularity": 492, + "codepoint": "1f44e_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fe", + "version": 1, + "popularity": 491, + "codepoint": "1f44e_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3ff", + "version": 1, + "popularity": 490, + "codepoint": "1f44e_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6", + "version": 1, + "popularity": 489, + "codepoint": "1faf6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fb", + "version": 1, + "popularity": 488, + "codepoint": "1faf6_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fc", + "version": 1, + "popularity": 487, + "codepoint": "1faf6_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fd", + "version": 1, + "popularity": 486, + "codepoint": "1faf6_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fe", + "version": 1, + "popularity": 485, + "codepoint": "1faf6_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3ff", + "version": 1, + "popularity": 484, + "codepoint": "1faf6_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c", + "version": 1, + "popularity": 483, + "codepoint": "1f64c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fb", + "version": 1, + "popularity": 482, + "codepoint": "1f64c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fc", + "version": 1, + "popularity": 481, + "codepoint": "1f64c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fd", + "version": 1, + "popularity": 480, + "codepoint": "1f64c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fe", + "version": 1, + "popularity": 479, + "codepoint": "1f64c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3ff", + "version": 1, + "popularity": 478, + "codepoint": "1f64c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450", + "version": 1, + "popularity": 477, + "codepoint": "1f450", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fb", + "version": 1, + "popularity": 476, + "codepoint": "1f450_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fc", + "version": 1, + "popularity": 475, + "codepoint": "1f450_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fd", + "version": 1, + "popularity": 474, + "codepoint": "1f450_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fe", + "version": 1, + "popularity": 473, + "codepoint": "1f450_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3ff", + "version": 1, + "popularity": 472, + "codepoint": "1f450_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932", + "version": 1, + "popularity": 471, + "codepoint": "1f932", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fb", + "version": 1, + "popularity": 470, + "codepoint": "1f932_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fc", + "version": 1, + "popularity": 469, + "codepoint": "1f932_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fd", + "version": 1, + "popularity": 468, + "codepoint": "1f932_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fe", + "version": 1, + "popularity": 467, + "codepoint": "1f932_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3ff", + "version": 1, + "popularity": 466, + "codepoint": "1f932_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c", + "version": 1, + "popularity": 465, + "codepoint": "1f91c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fb", + "version": 1, + "popularity": 464, + "codepoint": "1f91c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fc", + "version": 1, + "popularity": 463, + "codepoint": "1f91c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fd", + "version": 1, + "popularity": 462, + "codepoint": "1f91c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fe", + "version": 1, + "popularity": 461, + "codepoint": "1f91c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3ff", + "version": 1, + "popularity": 460, + "codepoint": "1f91c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b", + "version": 1, + "popularity": 459, + "codepoint": "1f91b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fb", + "version": 1, + "popularity": 458, + "codepoint": "1f91b_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fc", + "version": 1, + "popularity": 457, + "codepoint": "1f91b_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fd", + "version": 1, + "popularity": 456, + "codepoint": "1f91b_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fe", + "version": 1, + "popularity": 455, + "codepoint": "1f91b_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3ff", + "version": 1, + "popularity": 454, + "codepoint": "1f91b_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a", + "version": 1, + "popularity": 453, + "codepoint": "270a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fb", + "version": 1, + "popularity": 452, + "codepoint": "270a_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fc", + "version": 1, + "popularity": 451, + "codepoint": "270a_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fd", + "version": 1, + "popularity": 450, + "codepoint": "270a_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fe", + "version": 1, + "popularity": 449, + "codepoint": "270a_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3ff", + "version": 1, + "popularity": 448, + "codepoint": "270a_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a", + "version": 1, + "popularity": 447, + "codepoint": "1f44a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fb", + "version": 1, + "popularity": 446, + "codepoint": "1f44a_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fc", + "version": 1, + "popularity": 445, + "codepoint": "1f44a_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fd", + "version": 1, + "popularity": 444, + "codepoint": "1f44a_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fe", + "version": 1, + "popularity": 443, + "codepoint": "1f44a_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3ff", + "version": 1, + "popularity": 442, + "codepoint": "1f44a_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3", + "version": 1, + "popularity": 441, + "codepoint": "1faf3", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fb", + "version": 1, + "popularity": 440, + "codepoint": "1faf3_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fc", + "version": 1, + "popularity": 439, + "codepoint": "1faf3_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fd", + "version": 1, + "popularity": 438, + "codepoint": "1faf3_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fe", + "version": 1, + "popularity": 437, + "codepoint": "1faf3_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3ff", + "version": 1, + "popularity": 436, + "codepoint": "1faf3_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4", + "version": 1, + "popularity": 435, + "codepoint": "1faf4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fb", + "version": 1, + "popularity": 434, + "codepoint": "1faf4_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fc", + "version": 1, + "popularity": 433, + "codepoint": "1faf4_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fd", + "version": 1, + "popularity": 432, + "codepoint": "1faf4_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fe", + "version": 1, + "popularity": 431, + "codepoint": "1faf4_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3ff", + "version": 1, + "popularity": 430, + "codepoint": "1faf4_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1", + "version": 1, + "popularity": 429, + "codepoint": "1faf1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb", + "version": 1, + "popularity": 428, + "codepoint": "1faf1_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc", + "version": 1, + "popularity": 427, + "codepoint": "1faf1_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd", + "version": 1, + "popularity": 426, + "codepoint": "1faf1_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe", + "version": 1, + "popularity": 425, + "codepoint": "1faf1_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff", + "version": 1, + "popularity": 424, + "codepoint": "1faf1_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2", + "version": 1, + "popularity": 423, + "codepoint": "1faf2", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fb", + "version": 1, + "popularity": 422, + "codepoint": "1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fc", + "version": 1, + "popularity": 421, + "codepoint": "1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fd", + "version": 1, + "popularity": 420, + "codepoint": "1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fe", + "version": 1, + "popularity": 419, + "codepoint": "1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3ff", + "version": 1, + "popularity": 418, + "codepoint": "1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8", + "version": 1, + "popularity": 417, + "codepoint": "1faf8", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fb", + "version": 1, + "popularity": 416, + "codepoint": "1faf8_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fc", + "version": 1, + "popularity": 415, + "codepoint": "1faf8_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fd", + "version": 1, + "popularity": 414, + "codepoint": "1faf8_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fe", + "version": 1, + "popularity": 413, + "codepoint": "1faf8_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3ff", + "version": 1, + "popularity": 412, + "codepoint": "1faf8_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7", + "version": 1, + "popularity": 411, + "codepoint": "1faf7", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fb", + "version": 1, + "popularity": 410, + "codepoint": "1faf7_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fc", + "version": 1, + "popularity": 409, + "codepoint": "1faf7_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fd", + "version": 1, + "popularity": 408, + "codepoint": "1faf7_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fe", + "version": 1, + "popularity": 407, + "codepoint": "1faf7_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3ff", + "version": 1, + "popularity": 406, + "codepoint": "1faf7_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b", + "version": 1, + "popularity": 405, + "codepoint": "1f44b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fb", + "version": 1, + "popularity": 404, + "codepoint": "1f44b_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fc", + "version": 1, + "popularity": 403, + "codepoint": "1f44b_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fd", + "version": 1, + "popularity": 402, + "codepoint": "1f44b_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fe", + "version": 1, + "popularity": 401, + "codepoint": "1f44b_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3ff", + "version": 1, + "popularity": 400, + "codepoint": "1f44b_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a", + "version": 1, + "popularity": 399, + "codepoint": "1f91a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fb", + "version": 1, + "popularity": 398, + "codepoint": "1f91a_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fc", + "version": 1, + "popularity": 397, + "codepoint": "1f91a_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fd", + "version": 1, + "popularity": 396, + "codepoint": "1f91a_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fe", + "version": 1, + "popularity": 395, + "codepoint": "1f91a_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3ff", + "version": 1, + "popularity": 394, + "codepoint": "1f91a_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_fe0f", + "version": 1, + "popularity": 393, + "codepoint": "1f590_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fb", + "version": 1, + "popularity": 392, + "codepoint": "1f590_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fc", + "version": 1, + "popularity": 391, + "codepoint": "1f590_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fd", + "version": 1, + "popularity": 390, + "codepoint": "1f590_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fe", + "version": 1, + "popularity": 389, + "codepoint": "1f590_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3ff", + "version": 1, + "popularity": 388, + "codepoint": "1f590_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b", + "version": 1, + "popularity": 387, + "codepoint": "270b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fb", + "version": 1, + "popularity": 386, + "codepoint": "270b_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fc", + "version": 1, + "popularity": 385, + "codepoint": "270b_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fd", + "version": 1, + "popularity": 384, + "codepoint": "270b_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fe", + "version": 1, + "popularity": 383, + "codepoint": "270b_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3ff", + "version": 1, + "popularity": 382, + "codepoint": "270b_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596", + "version": 1, + "popularity": 381, + "codepoint": "1f596", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fb", + "version": 1, + "popularity": 380, + "codepoint": "1f596_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fc", + "version": 1, + "popularity": 379, + "codepoint": "1f596_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fd", + "version": 1, + "popularity": 378, + "codepoint": "1f596_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fe", + "version": 1, + "popularity": 377, + "codepoint": "1f596_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3ff", + "version": 1, + "popularity": 376, + "codepoint": "1f596_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f", + "version": 1, + "popularity": 375, + "codepoint": "1f91f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fb", + "version": 1, + "popularity": 374, + "codepoint": "1f91f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fc", + "version": 1, + "popularity": 373, + "codepoint": "1f91f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fd", + "version": 1, + "popularity": 372, + "codepoint": "1f91f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fe", + "version": 1, + "popularity": 371, + "codepoint": "1f91f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3ff", + "version": 1, + "popularity": 370, + "codepoint": "1f91f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918", + "version": 1, + "popularity": 369, + "codepoint": "1f918", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fb", + "version": 1, + "popularity": 368, + "codepoint": "1f918_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fc", + "version": 1, + "popularity": 367, + "codepoint": "1f918_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fd", + "version": 1, + "popularity": 366, + "codepoint": "1f918_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fe", + "version": 1, + "popularity": 365, + "codepoint": "1f918_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3ff", + "version": 1, + "popularity": 364, + "codepoint": "1f918_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_fe0f", + "version": 1, + "popularity": 363, + "codepoint": "270c_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fb", + "version": 1, + "popularity": 362, + "codepoint": "270c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fc", + "version": 1, + "popularity": 361, + "codepoint": "270c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fd", + "version": 1, + "popularity": 360, + "codepoint": "270c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fe", + "version": 1, + "popularity": 359, + "codepoint": "270c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3ff", + "version": 1, + "popularity": 358, + "codepoint": "270c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e", + "version": 1, + "popularity": 357, + "codepoint": "1f91e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fb", + "version": 1, + "popularity": 356, + "codepoint": "1f91e_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fc", + "version": 1, + "popularity": 355, + "codepoint": "1f91e_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fd", + "version": 1, + "popularity": 354, + "codepoint": "1f91e_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fe", + "version": 1, + "popularity": 353, + "codepoint": "1f91e_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3ff", + "version": 1, + "popularity": 352, + "codepoint": "1f91e_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0", + "version": 1, + "popularity": 351, + "codepoint": "1faf0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fb", + "version": 1, + "popularity": 350, + "codepoint": "1faf0_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fc", + "version": 1, + "popularity": 349, + "codepoint": "1faf0_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fd", + "version": 1, + "popularity": 348, + "codepoint": "1faf0_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fe", + "version": 1, + "popularity": 347, + "codepoint": "1faf0_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3ff", + "version": 1, + "popularity": 346, + "codepoint": "1faf0_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919", + "version": 1, + "popularity": 345, + "codepoint": "1f919", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fb", + "version": 1, + "popularity": 344, + "codepoint": "1f919_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fc", + "version": 1, + "popularity": 343, + "codepoint": "1f919_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fd", + "version": 1, + "popularity": 342, + "codepoint": "1f919_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fe", + "version": 1, + "popularity": 341, + "codepoint": "1f919_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3ff", + "version": 1, + "popularity": 340, + "codepoint": "1f919_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c", + "version": 1, + "popularity": 339, + "codepoint": "1f90c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fb", + "version": 1, + "popularity": 338, + "codepoint": "1f90c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fc", + "version": 1, + "popularity": 337, + "codepoint": "1f90c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fd", + "version": 1, + "popularity": 336, + "codepoint": "1f90c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fe", + "version": 1, + "popularity": 335, + "codepoint": "1f90c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3ff", + "version": 1, + "popularity": 334, + "codepoint": "1f90c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f", + "version": 1, + "popularity": 333, + "codepoint": "1f90f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fb", + "version": 1, + "popularity": 332, + "codepoint": "1f90f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fc", + "version": 1, + "popularity": 331, + "codepoint": "1f90f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fd", + "version": 1, + "popularity": 330, + "codepoint": "1f90f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fe", + "version": 1, + "popularity": 329, + "codepoint": "1f90f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3ff", + "version": 1, + "popularity": 328, + "codepoint": "1f90f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c", + "version": 1, + "popularity": 327, + "codepoint": "1f44c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fb", + "version": 1, + "popularity": 326, + "codepoint": "1f44c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fc", + "version": 1, + "popularity": 325, + "codepoint": "1f44c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fd", + "version": 1, + "popularity": 324, + "codepoint": "1f44c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fe", + "version": 1, + "popularity": 323, + "codepoint": "1f44c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3ff", + "version": 1, + "popularity": 322, + "codepoint": "1f44c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5", + "version": 1, + "popularity": 321, + "codepoint": "1faf5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fb", + "version": 1, + "popularity": 320, + "codepoint": "1faf5_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fc", + "version": 1, + "popularity": 319, + "codepoint": "1faf5_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fd", + "version": 1, + "popularity": 318, + "codepoint": "1faf5_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fe", + "version": 1, + "popularity": 317, + "codepoint": "1faf5_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3ff", + "version": 1, + "popularity": 316, + "codepoint": "1faf5_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449", + "version": 1, + "popularity": 315, + "codepoint": "1f449", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fb", + "version": 1, + "popularity": 314, + "codepoint": "1f449_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fc", + "version": 1, + "popularity": 313, + "codepoint": "1f449_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fd", + "version": 1, + "popularity": 312, + "codepoint": "1f449_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fe", + "version": 1, + "popularity": 311, + "codepoint": "1f449_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3ff", + "version": 1, + "popularity": 310, + "codepoint": "1f449_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448", + "version": 1, + "popularity": 309, + "codepoint": "1f448", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fb", + "version": 1, + "popularity": 308, + "codepoint": "1f448_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fc", + "version": 1, + "popularity": 307, + "codepoint": "1f448_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fd", + "version": 1, + "popularity": 306, + "codepoint": "1f448_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fe", + "version": 1, + "popularity": 305, + "codepoint": "1f448_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3ff", + "version": 1, + "popularity": 304, + "codepoint": "1f448_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_fe0f", + "version": 1, + "popularity": 303, + "codepoint": "261d_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fb", + "version": 1, + "popularity": 302, + "codepoint": "261d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fc", + "version": 1, + "popularity": 301, + "codepoint": "261d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fd", + "version": 1, + "popularity": 300, + "codepoint": "261d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fe", + "version": 1, + "popularity": 299, + "codepoint": "261d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3ff", + "version": 1, + "popularity": 298, + "codepoint": "261d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446", + "version": 1, + "popularity": 297, + "codepoint": "1f446", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fb", + "version": 1, + "popularity": 296, + "codepoint": "1f446_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fc", + "version": 1, + "popularity": 295, + "codepoint": "1f446_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fd", + "version": 1, + "popularity": 294, + "codepoint": "1f446_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fe", + "version": 1, + "popularity": 293, + "codepoint": "1f446_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3ff", + "version": 1, + "popularity": 292, + "codepoint": "1f446_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447", + "version": 1, + "popularity": 291, + "codepoint": "1f447", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fb", + "version": 1, + "popularity": 290, + "codepoint": "1f447_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fc", + "version": 1, + "popularity": 289, + "codepoint": "1f447_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fd", + "version": 1, + "popularity": 288, + "codepoint": "1f447_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fe", + "version": 1, + "popularity": 287, + "codepoint": "1f447_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3ff", + "version": 1, + "popularity": 286, + "codepoint": "1f447_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595", + "version": 1, + "popularity": 285, + "codepoint": "1f595", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fb", + "version": 1, + "popularity": 284, + "codepoint": "1f595_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fc", + "version": 1, + "popularity": 283, + "codepoint": "1f595_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fd", + "version": 1, + "popularity": 282, + "codepoint": "1f595_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fe", + "version": 1, + "popularity": 281, + "codepoint": "1f595_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3ff", + "version": 1, + "popularity": 280, + "codepoint": "1f595_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_fe0f", + "version": 1, + "popularity": 279, + "codepoint": "270d_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fb", + "version": 1, + "popularity": 278, + "codepoint": "270d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fc", + "version": 1, + "popularity": 277, + "codepoint": "270d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fd", + "version": 1, + "popularity": 276, + "codepoint": "270d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fe", + "version": 1, + "popularity": 275, + "codepoint": "270d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3ff", + "version": 1, + "popularity": 274, + "codepoint": "270d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933", + "version": 1, + "popularity": 273, + "codepoint": "1f933", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fb", + "version": 1, + "popularity": 272, + "codepoint": "1f933_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fc", + "version": 1, + "popularity": 271, + "codepoint": "1f933_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fd", + "version": 1, + "popularity": 270, + "codepoint": "1f933_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fe", + "version": 1, + "popularity": 269, + "codepoint": "1f933_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3ff", + "version": 1, + "popularity": 268, + "codepoint": "1f933_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f", + "version": 1, + "popularity": 267, + "codepoint": "1f64f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fb", + "version": 1, + "popularity": 266, + "codepoint": "1f64f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fc", + "version": 1, + "popularity": 265, + "codepoint": "1f64f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fd", + "version": 1, + "popularity": 264, + "codepoint": "1f64f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fe", + "version": 1, + "popularity": 263, + "codepoint": "1f64f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3ff", + "version": 1, + "popularity": 262, + "codepoint": "1f64f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f485", + "version": 1, + "popularity": 261, + "codepoint": "1f485", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fb", + "version": 1, + "popularity": 260, + "codepoint": "1f485_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fc", + "version": 1, + "popularity": 259, + "codepoint": "1f485_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fd", + "version": 1, + "popularity": 258, + "codepoint": "1f485_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fe", + "version": 1, + "popularity": 257, + "codepoint": "1f485_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3ff", + "version": 1, + "popularity": 256, + "codepoint": "1f485_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d", + "version": 1, + "popularity": 255, + "codepoint": "1f91d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fb", + "version": 1, + "popularity": 254, + "codepoint": "1f91d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3fc", + "version": 1, + "popularity": 253, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3fd", + "version": 1, + "popularity": 252, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3fe", + "version": 1, + "popularity": 251, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3ff", + "version": 1, + "popularity": 250, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3fb", + "version": 1, + "popularity": 249, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fc", + "version": 1, + "popularity": 248, + "codepoint": "1f91d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3fd", + "version": 1, + "popularity": 247, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3fe", + "version": 1, + "popularity": 246, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3ff", + "version": 1, + "popularity": 245, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3fb", + "version": 1, + "popularity": 244, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3fc", + "version": 1, + "popularity": 243, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fd", + "version": 1, + "popularity": 242, + "codepoint": "1f91d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3fe", + "version": 1, + "popularity": 241, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3ff", + "version": 1, + "popularity": 240, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3fb", + "version": 1, + "popularity": 239, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3fc", + "version": 1, + "popularity": 238, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3fd", + "version": 1, + "popularity": 237, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fe", + "version": 1, + "popularity": 236, + "codepoint": "1f91d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3ff", + "version": 1, + "popularity": 235, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fb", + "version": 1, + "popularity": 234, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fc", + "version": 1, + "popularity": 233, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fd", + "version": 1, + "popularity": 232, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fe", + "version": 1, + "popularity": 231, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3ff", + "version": 1, + "popularity": 230, + "codepoint": "1f91d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483", + "version": 1, + "popularity": 229, + "codepoint": "1f483", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fb", + "version": 1, + "popularity": 228, + "codepoint": "1f483_1f3fb", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fc", + "version": 1, + "popularity": 227, + "codepoint": "1f483_1f3fc", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fd", + "version": 1, + "popularity": 226, + "codepoint": "1f483_1f3fd", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fe", + "version": 1, + "popularity": 225, + "codepoint": "1f483_1f3fe", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3ff", + "version": 1, + "popularity": 224, + "codepoint": "1f483_1f3ff", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f490", + "version": 1, + "popularity": 223, + "codepoint": "1f490", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bouquet:", ":flowers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f339", + "version": 1, + "popularity": 222, + "codepoint": "1f339", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f940", + "version": 1, + "popularity": 221, + "codepoint": "1f940", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":wilted-flower:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f342", + "version": 1, + "popularity": 220, + "codepoint": "1f342", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":fallen-leaf:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f331", + "version": 1, + "popularity": 219, + "codepoint": "1f331", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":plant:", ":seed:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f343", + "version": 1, + "popularity": 218, + "codepoint": "1f343", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":leaves:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f340", + "version": 1, + "popularity": 217, + "codepoint": "1f340", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":luck:", ":four-leaf-clover:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fabe", + "version": 1, + "popularity": 216, + "codepoint": "1fabe", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":leafless-tree:"], + "sizes_px": [] + }, + { + "name": "emoji_u2744_fe0f", + "version": 1, + "popularity": 215, + "codepoint": "2744_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":snowflake:", ":winter:", ":cold:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30b", + "version": 1, + "popularity": 214, + "codepoint": "1f30b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":volcano:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f305", + "version": 1, + "popularity": 213, + "codepoint": "1f305", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":sunrise:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f304", + "version": 1, + "popularity": 212, + "codepoint": "1f304", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":sunrise-over-mountains:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f308", + "version": 1, + "popularity": 211, + "codepoint": "1f308", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rainbow:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae7", + "version": 1, + "popularity": 210, + "codepoint": "1fae7", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bubbles:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30a", + "version": 1, + "popularity": 209, + "codepoint": "1f30a", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":ocean:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f32c_fe0f", + "version": 1, + "popularity": 208, + "codepoint": "1f32c_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":wind-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f32a_fe0f", + "version": 1, + "popularity": 207, + "codepoint": "1f32a_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":tornado:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a7", + "version": 1, + "popularity": 206, + "codepoint": "1f4a7", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":droplet:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f327_fe0f", + "version": 1, + "popularity": 205, + "codepoint": "1f327_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rain-cloud:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f329_fe0f", + "version": 1, + "popularity": 204, + "codepoint": "1f329_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":cloud-with-lightning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30d", + "version": 1, + "popularity": 203, + "codepoint": "1f30d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":globe-showing-Europe-Africa:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30e", + "version": 1, + "popularity": 202, + "codepoint": "1f30e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":globe-showing-Americas:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30f", + "version": 1, + "popularity": 201, + "codepoint": "1f30f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":globe-showing-Asia-Australia:"], + "sizes_px": [] + }, + { + "name": "emoji_u2604_fe0f", + "version": 1, + "popularity": 200, + "codepoint": "2604_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":comet:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f42e", + "version": 1, + "popularity": 199, + "codepoint": "1f42e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":cow-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f984", + "version": 1, + "popularity": 198, + "codepoint": "1f984", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":unicorn:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f98e", + "version": 1, + "popularity": 197, + "codepoint": "1f98e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":lizard:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f409", + "version": 1, + "popularity": 196, + "codepoint": "1f409", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dragon:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f996", + "version": 1, + "popularity": 195, + "codepoint": "1f996", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":t-rex:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f995", + "version": 1, + "popularity": 194, + "codepoint": "1f995", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dinosaur:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f422", + "version": 1, + "popularity": 193, + "codepoint": "1f422", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":turtle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40a", + "version": 1, + "popularity": 192, + "codepoint": "1f40a", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":crocodile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40d", + "version": 1, + "popularity": 191, + "codepoint": "1f40d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":snake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f438", + "version": 1, + "popularity": 190, + "codepoint": "1f438", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":frog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f407", + "version": 1, + "popularity": 189, + "codepoint": "1f407", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rabbit:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f400", + "version": 1, + "popularity": 188, + "codepoint": "1f400", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f429", + "version": 1, + "popularity": 187, + "codepoint": "1f429", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":poodle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f415", + "version": 1, + "popularity": 186, + "codepoint": "1f415", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9ae", + "version": 1, + "popularity": 185, + "codepoint": "1f9ae", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":guide-dog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f415_200d_1f9ba", + "version": 1, + "popularity": 184, + "codepoint": "1f415_200d_1f9ba", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":service-dog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f416", + "version": 1, + "popularity": 183, + "codepoint": "1f416", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":pig:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40e", + "version": 1, + "popularity": 182, + "codepoint": "1f40e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":racehorse:"], + "sizes_px": [] + }, + { + "name": "emoji_u1facf", + "version": 1, + "popularity": 181, + "codepoint": "1facf", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":donkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f402", + "version": 1, + "popularity": 180, + "codepoint": "1f402", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":ox:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f410", + "version": 1, + "popularity": 179, + "codepoint": "1f410", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":goat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f998", + "version": 1, + "popularity": 178, + "codepoint": "1f998", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":kangaroo:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f405", + "version": 1, + "popularity": 177, + "codepoint": "1f405", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":tiger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f412", + "version": 1, + "popularity": 176, + "codepoint": "1f412", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f98d", + "version": 1, + "popularity": 175, + "codepoint": "1f98d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":gorilla:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9a7", + "version": 1, + "popularity": 174, + "codepoint": "1f9a7", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":orangutan:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f43f_fe0f", + "version": 1, + "popularity": 173, + "codepoint": "1f43f_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":chipmunk:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9a6", + "version": 1, + "popularity": 172, + "codepoint": "1f9a6", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":otter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f987", + "version": 1, + "popularity": 171, + "codepoint": "1f987", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f426", + "version": 1, + "popularity": 170, + "codepoint": "1f426", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bird:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f426_200d_2b1b", + "version": 1, + "popularity": 169, + "codepoint": "1f426_200d_2b1b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":black-bird:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f413", + "version": 1, + "popularity": 168, + "codepoint": "1f413", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rooster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f423", + "version": 1, + "popularity": 167, + "codepoint": "1f423", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":hatching-chick:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f424", + "version": 1, + "popularity": 166, + "codepoint": "1f424", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":baby-chick:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f425", + "version": 1, + "popularity": 165, + "codepoint": "1f425", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":hatched-chick:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f985", + "version": 1, + "popularity": 164, + "codepoint": "1f985", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":eagle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f989", + "version": 1, + "popularity": 163, + "codepoint": "1f989", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":owl:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f54a_fe0f", + "version": 1, + "popularity": 162, + "codepoint": "1f54a_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":peace:", ":dove:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fabf", + "version": 1, + "popularity": 161, + "codepoint": "1fabf", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":goose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f99a", + "version": 1, + "popularity": 160, + "codepoint": "1f99a", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":peacock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f426_200d_1f525", + "version": 1, + "popularity": 159, + "codepoint": "1f426_200d_1f525", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":phoenix:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9ad", + "version": 1, + "popularity": 158, + "codepoint": "1f9ad", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":seal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f988", + "version": 1, + "popularity": 157, + "codepoint": "1f988", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":shark:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f42c", + "version": 1, + "popularity": 156, + "codepoint": "1f42c", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dolphin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f433", + "version": 1, + "popularity": 155, + "codepoint": "1f433", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":whale:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41f", + "version": 1, + "popularity": 154, + "codepoint": "1f41f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":fish:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f421", + "version": 1, + "popularity": 153, + "codepoint": "1f421", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":blowfish:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f99e", + "version": 1, + "popularity": 152, + "codepoint": "1f99e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":lobster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f980", + "version": 1, + "popularity": 151, + "codepoint": "1f980", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":crab:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f419", + "version": 1, + "popularity": 150, + "codepoint": "1f419", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":octopus:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fabc", + "version": 1, + "popularity": 149, + "codepoint": "1fabc", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":jellyfish:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f982", + "version": 1, + "popularity": 148, + "codepoint": "1f982", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":scorpion:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f577_fe0f", + "version": 1, + "popularity": 147, + "codepoint": "1f577_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":spider:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40c", + "version": 1, + "popularity": 146, + "codepoint": "1f40c", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":snail:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41c", + "version": 1, + "popularity": 145, + "codepoint": "1f41c", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":ant:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f99f", + "version": 1, + "popularity": 144, + "codepoint": "1f99f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":mosquito:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fab3", + "version": 1, + "popularity": 143, + "codepoint": "1fab3", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":cockroach:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fab0", + "version": 1, + "popularity": 142, + "codepoint": "1fab0", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":fly:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41d", + "version": 1, + "popularity": 141, + "codepoint": "1f41d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bee:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41e", + "version": 1, + "popularity": 140, + "codepoint": "1f41e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":lady-bug:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f98b", + "version": 1, + "popularity": 139, + "codepoint": "1f98b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":butterfly:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41b", + "version": 1, + "popularity": 138, + "codepoint": "1f41b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bug:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fab1", + "version": 1, + "popularity": 137, + "codepoint": "1fab1", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":worm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f43e", + "version": 1, + "popularity": 136, + "codepoint": "1f43e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":paw prints:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f345", + "version": 1, + "popularity": 135, + "codepoint": "1f345", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":tomato:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fadc", + "version": 1, + "popularity": 134, + "codepoint": "1fadc", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":root-vegetable:", ":beet:", ":turnip:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f373", + "version": 1, + "popularity": 133, + "codepoint": "1f373", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":cooking:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f32f", + "version": 1, + "popularity": 132, + "codepoint": "1f32f", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":burrito:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f35d", + "version": 1, + "popularity": 131, + "codepoint": "1f35d", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":spaghetti:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f35c", + "version": 1, + "popularity": 130, + "codepoint": "1f35c", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":steaming-bowl:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f37f", + "version": 1, + "popularity": 129, + "codepoint": "1f37f", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":popcorn:"], + "sizes_px": [] + }, + { + "name": "emoji_u2615", + "version": 1, + "popularity": 128, + "codepoint": "2615", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":hot-beverage:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f37b", + "version": 1, + "popularity": 127, + "codepoint": "1f37b", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":clinking-beer-mugs:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f942", + "version": 1, + "popularity": 126, + "codepoint": "1f942", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":clinking-glasses:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f37e", + "version": 1, + "popularity": 125, + "codepoint": "1f37e", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":bottle-with-popping-cork:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f377", + "version": 1, + "popularity": 124, + "codepoint": "1f377", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":wine-glass:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fad7", + "version": 1, + "popularity": 123, + "codepoint": "1fad7", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":pour:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f379", + "version": 1, + "popularity": 122, + "codepoint": "1f379", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":tropical-drink:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6a7", + "version": 1, + "popularity": 121, + "codepoint": "1f6a7", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":construction:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6a8", + "version": 1, + "popularity": 120, + "codepoint": "1f6a8", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":police-car-light:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6b2", + "version": 1, + "popularity": 119, + "codepoint": "1f6b2", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":bicycle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f697", + "version": 1, + "popularity": 118, + "codepoint": "1f697", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":automobile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3ce_fe0f", + "version": 1, + "popularity": 117, + "codepoint": "1f3ce_fe0f", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":racing-car:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f695", + "version": 1, + "popularity": 116, + "codepoint": "1f695", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":taxi:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f68c", + "version": 1, + "popularity": 115, + "codepoint": "1f68c", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":bus:"], + "sizes_px": [] + }, + { + "name": "emoji_u26f5", + "version": 1, + "popularity": 114, + "codepoint": "26f5", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":sailboat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6f6", + "version": 1, + "popularity": 113, + "codepoint": "1f6f6", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":canoe:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6f8", + "version": 1, + "popularity": 112, + "codepoint": "1f6f8", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":flying-saucer:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f680", + "version": 1, + "popularity": 111, + "codepoint": "1f680", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":rocket:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6eb", + "version": 1, + "popularity": 110, + "codepoint": "1f6eb", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":airplane-departure:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6ec", + "version": 1, + "popularity": 109, + "codepoint": "1f6ec", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":airplane-arrival:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3a2", + "version": 1, + "popularity": 108, + "codepoint": "1f3a2", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":roller-coaster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3a1", + "version": 1, + "popularity": 107, + "codepoint": "1f3a1", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":ferris-wheel:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d5_fe0f", + "version": 1, + "popularity": 106, + "codepoint": "1f3d5_fe0f", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":camping:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f388", + "version": 1, + "popularity": 105, + "codepoint": "1f388", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":balloon:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f382", + "version": 1, + "popularity": 104, + "codepoint": "1f382", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":birthday-cake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f381", + "version": 1, + "popularity": 103, + "codepoint": "1f381", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":wrapped-gift:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f386", + "version": 1, + "popularity": 102, + "codepoint": "1f386", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":fireworks:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa85", + "version": 1, + "popularity": 101, + "codepoint": "1fa85", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":piñata:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faa9", + "version": 1, + "popularity": 100, + "codepoint": "1faa9", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":mirror-ball:", ":disco-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f947", + "version": 1, + "popularity": 99, + "codepoint": "1f947", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":gold-medal:", ":1st-place-medal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f948", + "version": 1, + "popularity": 98, + "codepoint": "1f948", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":silver-medal:", ":2nd-place-medal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f949", + "version": 1, + "popularity": 97, + "codepoint": "1f949", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":bronze-medal:", ":3rd-place-medal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3c6", + "version": 1, + "popularity": 96, + "codepoint": "1f3c6", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":trophy:"], + "sizes_px": [] + }, + { + "name": "emoji_u26bd", + "version": 1, + "popularity": 95, + "codepoint": "26bd", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":soccer-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u26be", + "version": 1, + "popularity": 94, + "codepoint": "26be", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":baseball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94e", + "version": 1, + "popularity": 93, + "codepoint": "1f94e", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":softball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3be", + "version": 1, + "popularity": 92, + "codepoint": "1f3be", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":tennis:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3f8", + "version": 1, + "popularity": 91, + "codepoint": "1f3f8", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":badminton:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94d", + "version": 1, + "popularity": 90, + "codepoint": "1f94d", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":lacrosse:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3cf", + "version": 1, + "popularity": 89, + "codepoint": "1f3cf", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":cricket-game:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d1", + "version": 1, + "popularity": 88, + "codepoint": "1f3d1", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":field-hockey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d2", + "version": 1, + "popularity": 87, + "codepoint": "1f3d2", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ice-hockey:"], + "sizes_px": [] + }, + { + "name": "emoji_u26f8_fe0f", + "version": 1, + "popularity": 86, + "codepoint": "26f8_fe0f", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ice-skate:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6fc", + "version": 1, + "popularity": 85, + "codepoint": "1f6fc", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":roller-skates:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa70", + "version": 1, + "popularity": 84, + "codepoint": "1fa70", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ballet-shoes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6f9", + "version": 1, + "popularity": 83, + "codepoint": "1f6f9", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":skateboard:"], + "sizes_px": [] + }, + { + "name": "emoji_u26f3", + "version": 1, + "popularity": 82, + "codepoint": "26f3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":flag-in-hole:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3af", + "version": 1, + "popularity": 81, + "codepoint": "1f3af", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":direct-hit:", ":target:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94f", + "version": 1, + "popularity": 80, + "codepoint": "1f94f", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":flying-disc:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa83", + "version": 1, + "popularity": 79, + "codepoint": "1fa83", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":boomerang:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa81", + "version": 1, + "popularity": 78, + "codepoint": "1fa81", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":kite:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3a3", + "version": 1, + "popularity": 77, + "codepoint": "1f3a3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":fishing-pole:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94b", + "version": 1, + "popularity": 76, + "codepoint": "1f94b", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":martial-arts-uniform:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b1", + "version": 1, + "popularity": 75, + "codepoint": "1f3b1", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":8-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d3", + "version": 1, + "popularity": 74, + "codepoint": "1f3d3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ping-pong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b3", + "version": 1, + "popularity": 73, + "codepoint": "1f3b3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":bowling:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b2", + "version": 1, + "popularity": 72, + "codepoint": "1f3b2", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":die:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b0", + "version": 1, + "popularity": 71, + "codepoint": "1f3b0", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":slot-machine:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa84", + "version": 1, + "popularity": 70, + "codepoint": "1fa84", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":wand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4f8", + "version": 1, + "popularity": 69, + "codepoint": "1f4f8", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":camera-flash:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fadf", + "version": 1, + "popularity": 68, + "codepoint": "1fadf", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":splatter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b7", + "version": 1, + "popularity": 67, + "codepoint": "1f3b7", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":saxophone:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3ba", + "version": 1, + "popularity": 66, + "codepoint": "1f3ba", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":trumpet:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3bb", + "version": 1, + "popularity": 65, + "codepoint": "1f3bb", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":violin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa89", + "version": 1, + "popularity": 64, + "codepoint": "1fa89", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":harp:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f941", + "version": 1, + "popularity": 63, + "codepoint": "1f941", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":drum:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa87", + "version": 1, + "popularity": 62, + "codepoint": "1fa87", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":maracas:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3ac", + "version": 1, + "popularity": 61, + "codepoint": "1f3ac", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":clapper:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f50b", + "version": 1, + "popularity": 60, + "codepoint": "1f50b", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":battery-full:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faab", + "version": 1, + "popularity": 59, + "codepoint": "1faab", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":battery-low:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa99", + "version": 1, + "popularity": 58, + "codepoint": "1fa99", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":coin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4b8", + "version": 1, + "popularity": 57, + "codepoint": "1f4b8", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":money-with-wings:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48e", + "version": 1, + "popularity": 56, + "codepoint": "1f48e", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":gem-stone:"], + "sizes_px": [] + }, + { + "name": "emoji_u2696_fe0f", + "version": 1, + "popularity": 55, + "codepoint": "2696_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":balance-scale:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a1", + "version": 1, + "popularity": 54, + "codepoint": "1f4a1", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":light-bulb:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f393", + "version": 1, + "popularity": 53, + "codepoint": "1f393", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":graduation-cap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48d", + "version": 1, + "popularity": 52, + "codepoint": "1f48d", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":ring:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faad", + "version": 1, + "popularity": 51, + "codepoint": "1faad", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":fan:"], + "sizes_px": [] + }, + { + "name": "emoji_u2602_fe0f", + "version": 1, + "popularity": 50, + "codepoint": "2602_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":umbrella:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa8f", + "version": 1, + "popularity": 49, + "codepoint": "1fa8f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":shovel:", ":dig:"], + "sizes_px": [] + }, + { + "name": "emoji_u2699_fe0f", + "version": 1, + "popularity": 48, + "codepoint": "2699_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":gear:"], + "sizes_px": [] + }, + { + "name": "emoji_u26d3_fe0f_200d_1f4a5", + "version": 1, + "popularity": 47, + "codepoint": "26d3_fe0f_200d_1f4a5", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":broken-chain:"], + "sizes_px": [] + }, + { + "name": "emoji_u270f_fe0f", + "version": 1, + "popularity": 46, + "codepoint": "270f_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":pencil:"], + "sizes_px": [] + }, + { + "name": "emoji_u23f0", + "version": 1, + "popularity": 45, + "codepoint": "23f0", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":alarm-clock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6ce_fe0f", + "version": 1, + "popularity": 44, + "codepoint": "1f6ce_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":bellhop-bell:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f514", + "version": 1, + "popularity": 43, + "codepoint": "1f514", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":bell:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f52e", + "version": 1, + "popularity": 42, + "codepoint": "1f52e", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":crystal-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a3", + "version": 1, + "popularity": 41, + "codepoint": "1f4a3", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":bomb:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faa4", + "version": 1, + "popularity": 40, + "codepoint": "1faa4", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":mouse-trap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f512", + "version": 1, + "popularity": 39, + "codepoint": "1f512", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":locked:"], + "sizes_px": [] + }, + { + "name": "emoji_u2648", + "version": 1, + "popularity": 38, + "codepoint": "2648", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Aries:"], + "sizes_px": [] + }, + { + "name": "emoji_u2649", + "version": 1, + "popularity": 37, + "codepoint": "2649", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Taurus:"], + "sizes_px": [] + }, + { + "name": "emoji_u264a", + "version": 1, + "popularity": 36, + "codepoint": "264a", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Gemini:"], + "sizes_px": [] + }, + { + "name": "emoji_u264b", + "version": 1, + "popularity": 35, + "codepoint": "264b", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Cancer:"], + "sizes_px": [] + }, + { + "name": "emoji_u264c", + "version": 1, + "popularity": 34, + "codepoint": "264c", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Leo:"], + "sizes_px": [] + }, + { + "name": "emoji_u264d", + "version": 1, + "popularity": 33, + "codepoint": "264d", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Virgo:"], + "sizes_px": [] + }, + { + "name": "emoji_u264e", + "version": 1, + "popularity": 32, + "codepoint": "264e", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Libra:"], + "sizes_px": [] + }, + { + "name": "emoji_u264f", + "version": 1, + "popularity": 31, + "codepoint": "264f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Scorpio:"], + "sizes_px": [] + }, + { + "name": "emoji_u2650", + "version": 1, + "popularity": 30, + "codepoint": "2650", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Sagittarius:"], + "sizes_px": [] + }, + { + "name": "emoji_u2651", + "version": 1, + "popularity": 29, + "codepoint": "2651", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Capricorn:"], + "sizes_px": [] + }, + { + "name": "emoji_u2652", + "version": 1, + "popularity": 28, + "codepoint": "2652", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Aquarius:"], + "sizes_px": [] + }, + { + "name": "emoji_u2653", + "version": 1, + "popularity": 27, + "codepoint": "2653", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Pisces:"], + "sizes_px": [] + }, + { + "name": "emoji_u26ce", + "version": 1, + "popularity": 26, + "codepoint": "26ce", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Ophiuchus:"], + "sizes_px": [] + }, + { + "name": "emoji_u2757", + "version": 1, + "popularity": 25, + "codepoint": "2757", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":exclamation:", ":exclamation-mark:"], + "sizes_px": [] + }, + { + "name": "emoji_u2753", + "version": 1, + "popularity": 24, + "codepoint": "2753", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":question:", ":question-mark:", ":?:"], + "sizes_px": [] + }, + { + "name": "emoji_u2049_fe0f", + "version": 1, + "popularity": 23, + "codepoint": "2049_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":exclamation-question-mark:", ":!?:"], + "sizes_px": [] + }, + { + "name": "emoji_u203c_fe0f", + "version": 1, + "popularity": 22, + "codepoint": "203c_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":exclamation-double:", ":!!:"], + "sizes_px": [] + }, + { + "name": "emoji_u274c", + "version": 1, + "popularity": 21, + "codepoint": "274c", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":cross-mark:", ":x:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f198", + "version": 1, + "popularity": 20, + "codepoint": "1f198", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":sos:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4f4", + "version": 1, + "popularity": 19, + "codepoint": "1f4f4", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":phone-off:"], + "sizes_px": [] + }, + { + "name": "emoji_u2622_fe0f", + "version": 1, + "popularity": 18, + "codepoint": "2622_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":radioactive:"], + "sizes_px": [] + }, + { + "name": "emoji_u2623_fe0f", + "version": 1, + "popularity": 17, + "codepoint": "2623_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":biohazard:"], + "sizes_px": [] + }, + { + "name": "emoji_u26a0_fe0f", + "version": 1, + "popularity": 16, + "codepoint": "26a0_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":warning:"], + "sizes_px": [] + }, + { + "name": "emoji_u2705", + "version": 1, + "popularity": 15, + "codepoint": "2705", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":check-mark:", ":check-mark-green:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f195", + "version": 1, + "popularity": 14, + "codepoint": "1f195", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":new:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f193", + "version": 1, + "popularity": 13, + "codepoint": "1f193", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":free:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f199", + "version": 1, + "popularity": 12, + "codepoint": "1f199", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":up!:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f192", + "version": 1, + "popularity": 11, + "codepoint": "1f192", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":cool:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6ae", + "version": 1, + "popularity": 10, + "codepoint": "1f6ae", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":litter:"], + "sizes_px": [] + }, + { + "name": "emoji_u262e_fe0f", + "version": 1, + "popularity": 9, + "codepoint": "262e_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":peace-symbol:"], + "sizes_px": [] + }, + { + "name": "emoji_u262f_fe0f", + "version": 1, + "popularity": 8, + "codepoint": "262f_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":yin-yang:"], + "sizes_px": [] + }, + { + "name": "emoji_u267e_fe0f", + "version": 1, + "popularity": 7, + "codepoint": "267e_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":infinity:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b6", + "version": 1, + "popularity": 6, + "codepoint": "1f3b6", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":musical-notes:"], + "sizes_px": [] + }, + { + "name": "emoji_u2795", + "version": 1, + "popularity": 5, + "codepoint": "2795", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":plus-sign:", ":+:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3c1", + "version": 1, + "popularity": 4, + "codepoint": "1f3c1", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":chequered-flag:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6a9", + "version": 1, + "popularity": 3, + "codepoint": "1f6a9", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":triangular-flag:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3f4", + "version": 1, + "popularity": 2, + "codepoint": "1f3f4", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":black-flag:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3f3_fe0f", + "version": 1, + "popularity": 1, + "codepoint": "1f3f3_fe0f", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":white-flag:"], + "sizes_px": [] + } + ] +} diff --git a/examples/sveltekit-group-chat/src/lib/emojis/index.ts b/examples/sveltekit-group-chat/src/lib/emojis/index.ts new file mode 100644 index 0000000..c9c843f --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/emojis/index.ts @@ -0,0 +1,23 @@ +// animated emojis from +// https://googlefonts.github.io/noto-emoji-animation/ + +import icons from './icons.json'; + +export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined { + if (!emoji) return; + // Convert emoji to lowercase hex codepoints joined by "_" + const codepoints: string[] = []; + for (const char of emoji) { + codepoints.push(char.codePointAt(0)!.toString(16).toLowerCase()); + } + + let key = codepoints.join('_'); + + if (icons.icons.find((v) => v.codepoint == key)) { + return `https://fonts.gstatic.com/s/e/notoemoji/latest/${key}/512.webp`; + } + + key = codepoints.filter((cp) => cp !== 'fe0f' && cp !== 'fe0e').join('_'); + if (icons.icons.find((v) => v.codepoint == key)) + return `https://fonts.gstatic.com/s/e/notoemoji/latest/${key}/512.webp`; +} diff --git a/examples/sveltekit-group-chat/src/lib/index.ts b/examples/sveltekit-group-chat/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/examples/sveltekit-group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts b/examples/sveltekit-group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts new file mode 100644 index 0000000..44794e4 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import { createOAuthClient } from '$lib/atproto/server/oauth'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ platform }) => { + const oauth = createOAuthClient(platform?.env); + return json(oauth.metadata); +}; diff --git a/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/callback/+server.ts b/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/callback/+server.ts new file mode 100644 index 0000000..f6a71e8 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/callback/+server.ts @@ -0,0 +1,41 @@ +import { redirect } from '@sveltejs/kit'; +import { createOAuthClient } from '$lib/atproto/server/oauth'; +import { setSignedCookie } from '$lib/atproto/server/signed-cookie'; +import { scopes } from '$lib/atproto/settings'; +import { dev } from '$app/environment'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url, platform, cookies }) => { + const oauth = createOAuthClient(platform?.env); + + // oauth.callback() validates the state parameter (CSRF protection) and + // exchanges the authorization code for tokens via the token endpoint. + try { + const { session } = await oauth.callback(url.searchParams); + + const cookieOpts = { + path: '/', + httpOnly: true, + secure: !dev, + sameSite: 'lax' as const, + maxAge: 60 * 60 * 24 * 180 // 180 days + }; + + setSignedCookie(cookies, 'did', session.did, cookieOpts); + setSignedCookie(cookies, 'scope', scopes.join(' '), cookieOpts); + } catch (e) { + console.error('OAuth callback failed:', e); + redirect(303, '/?error=auth_failed'); + } + + const returnTo = cookies.get('oauth_return_to'); + if (returnTo) { + cookies.delete('oauth_return_to', { path: '/' }); + const decoded = decodeURIComponent(returnTo); + if (decoded.startsWith('/') && !decoded.startsWith('//')) { + redirect(303, decoded); + } + } + + redirect(303, '/'); +}; diff --git a/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts b/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts new file mode 100644 index 0000000..ae4dd84 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import { createOAuthClient } from '$lib/atproto/server/oauth'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ platform }) => { + const oauth = createOAuthClient(platform?.env); + return json(oauth.jwks ?? { keys: [] }); +}; diff --git a/examples/sveltekit-group-chat/src/routes/+layout.server.ts b/examples/sveltekit-group-chat/src/routes/+layout.server.ts new file mode 100644 index 0000000..756dc10 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/+layout.server.ts @@ -0,0 +1,25 @@ +import type { LayoutServerLoad } from './$types'; +import { getServerClient } from '$lib/contrail'; +import { extractProfile } from '$lib/contrail/client'; + +export const load: LayoutServerLoad = async ({ locals, platform }) => { + if (!locals.did || !locals.client) { + return { did: null, profile: null }; + } + + try { + const client = getServerClient(platform!.env.DB); + const res = await client.get('statusphere.app.getProfile', { + params: { actor: locals.did } + }); + + if (!res.ok) return { did: locals.did, profile: null }; + + return { + did: locals.did, + profile: extractProfile(res.data) + }; + } catch { + return { did: locals.did, profile: null }; + } +}; diff --git a/examples/sveltekit-group-chat/src/routes/+layout.svelte b/examples/sveltekit-group-chat/src/routes/+layout.svelte new file mode 100644 index 0000000..3b11797 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/+layout.svelte @@ -0,0 +1,22 @@ + + +{@render children()} + + + + + + diff --git a/examples/sveltekit-group-chat/src/routes/+layout.ts b/examples/sveltekit-group-chat/src/routes/+layout.ts new file mode 100644 index 0000000..77ab0a0 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/+layout.ts @@ -0,0 +1 @@ +export const ssr = true; diff --git a/examples/sveltekit-group-chat/src/routes/+page.server.ts b/examples/sveltekit-group-chat/src/routes/+page.server.ts new file mode 100644 index 0000000..a9bb158 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/+page.server.ts @@ -0,0 +1,42 @@ +import { getServerClient } from '$lib/contrail'; +import { extractProfile, type Profile } from '$lib/contrail/client'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ platform }) => { + try { + const client = getServerClient(platform!.env.DB); + + const res = await client.get('statusphere.app.status.listRecords', { + params: { limit: 50, profiles: true, sort: 'createdAt', order: 'desc' } + }); + + if (!res.ok) return { statuses: [], profiles: {} }; + + const records = res.data.records; + const statuses = records + .map((r) => { + const record = r.record as { status: string; createdAt: string }; + return { + did: r.did, + rkey: r.rkey, + status: record.status, + createdAt: record.createdAt + }; + }) + .filter((s) => !isNaN(new Date(s.createdAt).getTime())); + + const profiles: Record = {}; + if (res.data.profiles) { + for (const p of res.data.profiles) { + profiles[p.did] = extractProfile(p); + } + } + + return { statuses, profiles }; + } catch { + return { + statuses: [] as { did: string; rkey: string; status: string; createdAt: string }[], + profiles: {} as Record + }; + } +}; diff --git a/examples/sveltekit-group-chat/src/routes/+page.svelte b/examples/sveltekit-group-chat/src/routes/+page.svelte new file mode 100644 index 0000000..0225014 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/+page.svelte @@ -0,0 +1,188 @@ + + +
+

svelte + cloudflare workers statusphere

+ + + + {#if !user.isLoggedIn} + + {:else} +
+ { + const rkey = createTID(); + const createdAt = new Date().toISOString(); + const key = `${user.did!}-${rkey}`; + seenKeys.add(key); + localStatuses = [ + { did: user.did!, rkey, status: emoji.unicode, createdAt }, + ...localStatuses + ]; + open = false; + await putRecord({ + rkey, + collection: 'xyz.statusphere.status', + record: { + status: emoji.unicode, + createdAt + } + }); + }} + bind:open + > + {#snippet child({ props })} + + {/snippet} + + +
+ {/if} + + {#if allStatuses.length > 0} +
    + {#each allStatuses as status, i (`${status.did}-${status.rkey}`)} + {@const profile = + profiles[status.did] ?? + (status.did === user.did && data.profile + ? { displayName: data.profile.displayName, handle: data.profile.handle } + : null)} + {@const animated = emojiToNotoAnimatedWebp(status.status)} +
  • +
    +
    + {#if animated} + {#if i === 0} + {status.status} + {:else} + {status.status} + {/if} + {:else if i === 0} + {status.status} + {:else} + {status.status} + {/if} +
    + {#if i < allStatuses.length - 1} +
    + {/if} +
    +
    + {#if profile} + {profile.displayName || profile.handle} + {:else} + {status.did.slice(0, 20)}... + {/if} + · + + + +
    +
  • + {/each} +
+ {/if} +
diff --git a/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts b/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts new file mode 100644 index 0000000..77ff19e --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts @@ -0,0 +1,15 @@ +import { contrail, ensureInit } from '$lib/contrail'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request, platform }) => { + const secret = request.headers.get('X-Cron-Secret'); + if (secret !== platform!.env.CRON_SECRET) { + return new Response('Unauthorized', { status: 401 }); + } + + const db = platform!.env.DB; + await ensureInit(db); + await contrail.ingest({}, db); + + return new Response('OK'); +}; diff --git a/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts b/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts new file mode 100644 index 0000000..400502c --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts @@ -0,0 +1,14 @@ +import { createHandler } from '@atmo-dev/contrail/server'; +import { contrail, ensureInit } from '$lib/contrail'; +import type { RequestHandler } from './$types'; + +const handle = createHandler(contrail); + +async function handler(request: Request, platform: App.Platform | undefined) { + const db = platform!.env.DB; + await ensureInit(db); + return handle(request, db) as Promise; +} + +export const GET: RequestHandler = async ({ request, platform }) => handler(request, platform); +export const POST: RequestHandler = async ({ request, platform }) => handler(request, platform); diff --git a/examples/sveltekit-group-chat/static/favicon.png b/examples/sveltekit-group-chat/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4i*LmhD`I74h#$ojKx9jP7LeL$-HD>V7=?< z;uunK>uv1q+}9QYp`3aLRhP6(c_0+yMGvW%`8w9Z4+AO$r=LiV^}WjuyMS z-`u@3yY$$SKDCAN>H9tj&u}g}XS4E2pY^*PkNZ3lroTLubH?oMf4#J9vzMpzCckct zowYnCeNLl>-tC#oR|mW7^83!__-WF*w@N!}ZnWO)*sV9e?K!tS_p_50%zk=doa_6a z9Ze7`Oq(1d_U2FyZ`xGx-xFfEE={d84w$f9rS697HscGE-dGl;{V=G?`XscL>&++e z>-U#%oq6{6`Q0U4b%OVNS0!jYj1*aM;g7Z{*A0W>9o{wqst=zQyeQ;ij+$byPs!T7 zP$;gE@43N(Z#KN^`%g~qnDC*fQ@9u5T z`010b%(4WBW*Igy)jqkB43?QUzqIP?pR?n;uR)>M{Z!UJzVqjLRQx?D9Xlud=JZMH zM4Dt}4@n0eu2Ixgk=cKk=N7x!uIU#Qr)QK{iRNADxA(7D>^{r+(1Q28%d+>r`1fM! z3!#PnEzgzrWhp(JYGANY`@x@o);^8RydN}I>CasHT;#z0|7n-*i5nc8wD_Qhgrn&1 z_=)HIc7)82SrZn{n5O;yq=&Wj!=(QfoT>{MZZVX#Y(9Ll{di*M8HGwlu2VN3EWtQvP&QN2PVR8N7!YC#E{m*$kY8i3^zQiv$JVzqYQu{#r;icEl z=UhCnkiGRom%yd1b*>!Dbz(=$xA=OCK9sWW?kjHnIi=(8N8<^v!`m)TWlU#3bX%EA zWW|L?Hd)uCpL{-aJW{t>_loq&RRZ@V7u56iZFu;9u7AgewTA2m_dYzmg!LfHN%jio z6)Tz8tu4PtytF=2_HyzK35Her8CehRz9{eceYP>8gyZOsf1e+R9eAyLo&U`AQ!>jL z>i##s-geN-*K&%b<6Sll}2Yvce^~*xG<6XX?1V3gp#~N zkiz|sN2i}Ey4w@5>yt(|hpKnhq#5tcY68zPo5(r8-F4X1_Ok2&?FGW>zrU(?x;Q#c?bx4-5dPgKtZr*@mZ=`3%X zy6%Z`^e5Hr^Y+>AzJm9>p2Z^BF4@_(4b1il6W?8xzhU;SdYz;2?R&rX$y{t=F!Y`E zR$}w<@9ihF9(X_5v`6rxTqRqD=!eToZeQ5@{QQC0#~w>6ukx<+wTnGF|CY_6xJ%zz zHr<`$x>nEcQ1ZHi?IAA|7rHVEt3Nz@U3=2vrr*f|MGoxkLSkxhhdzJg^6Z@Zq2`!6 zN9X(x&t6IHdUbyJoZ{}h2h3KD+a6rvIIkc7=B91g%$DcR#pk?L*qd_b^D~z6tu4>l zcb}QJe%gmWZyk5-zxe)Gtrh1xH}U_oBV*EcOaCc*(s}2g+Fj}Irl)7WovQcub7w{E upWAk8&OVF%_w#jD-*4ISJB6Y@xbv5oeJi^*U66r+fx*+&&t;ucLK6TBI`Z29 literal 0 HcmV?d00001 diff --git a/examples/sveltekit-group-chat/static/og.png b/examples/sveltekit-group-chat/static/og.png new file mode 100644 index 0000000000000000000000000000000000000000..62cbbc5fad01c8f7605857bca64b1066af12afb8 GIT binary patch literal 61667 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz*NS;#K6FCKkpFz}$*?!C{p zzHXb_(Z#CP)!94c?%M0`^m1520s;glgjg-T^8EC<()!Km(^3Ni=k1>VU4Gw<&BEzP z*FWE!X}NsHLPu9Y1~AZ=pTyAuW-*-NY!m>q96S{TK+a@f(9mRY0t+#ywmNWtSqqj3 za4>>dQ$&~)!HkAUP6{kwRzMKUFfXoA!$!l5frO;-L-N#g{rg*Hitc~%`MLi@LBYDZ zy8WxX{)Zll^*wa>-rwV8r5_(1-TmjAbZzrT?&Ku9AHROo{QBZ)v3KLfjj4ZsO`G<$ z*Vkvu)~()C^`_37CMPfd{O^(K)6aZO8rRL6*V@|J)umOx?!yKH`~A;bcPV}-o}wGQ zt*)8v)hnq#hqi3FV!b{8|0*p;1_lv9j+PI`Ti#zteEm)K&!jnX<`^&cKR?UxZ+Pg- z{a!m(pX_{m{D1Q9U0Y_&y5BwB=i%Gi+n4#wnlfwFsVU}}w@SXv*(o+{nyS~7gnL{S zKQ~o}b4^?1H+T1%;?^7;AsngV{at)9XhuyV5c|K#1XCe6Nonqk?8b8l~( zT@wxt3i{E?D`jR=^JV@dRqsWa$>bwgK){J+~Rt{34i@!wgSZ@=5)W4?E6 zY`$oD{jC?5mcG3u@Nj&nDg#4<5>ul<9h)mtVyBSmG`;wX8-vyVrNs5iuP=%c6cYMW z{OYHCgTo2_7LgC{?5ox+T{`u8kpRb37EmeR5GTO#Fg-v;VAYBhJ1TSD+*!(9Z}eW> zf0|y|m#<&{dW!!2&)+yfp2NEAmCByRY43k-PTyPoRYXzX-*s?V^Mn;-HOry-Y#>!{ z)6YxV7b+P?23(MS{`K|xxP3Z8_NPykIyF1|xZegUh$e`$IQ@_m;HZ~kajH{@*;VsX zzW(1)pKU1{AD?@Bd!`f1q5o!}qN`z#g218k4GuppD+qjIUU+#`XfqpsqMG!#=kw>y zE1!X)PONk)FR!n!@9Gf4S66m!Ubk-Dwryn_A03@MdGcZayH4%afib@7?pibm>ydY6BCKEoE9-f(SNi(eFW@gJo_tr{3dHwkD`}gj*<;2C+?FFPHLb5dSUi|;~JpOF-mISrv z-BqULZk-hnrJh<_nUwD5 zud}aTym;|uWaOEiEr)l1e{oPG$aK$qn}x}D*H)L``Y_8htM=cgmmdx`%e{MdW{RrU zloj_EFv@P)Rr*>|Qu5{N*TITwUpSxntsdw9B4qjADU&BFZ$4*antQEhU&Ig9`F6GY zDjx1BsD7D~^36ZN;lXV4HxD;&*s>;QDcAm%eKjvmyZm?>5a+&}@9m*oU$d^QbCr+& z6?XE>84n*1j;hbkZf<^FvcTEdxuv(TR&pYPS;{=mC;sA=0;@YE zI!sLQcJ;q>OiV(~ZCo(-WKZM4xy(|tq-?9cyt=Sbc`kFwDi@a)emRQ<`U78Zw@a8ZGBTb$NxA%H%d6AV^^P7p_T%^O%Xw3F#xXE3D6$+{)O~Au?Cw%| z+cF*2Lq@5m0vmhK zvQCTW#cW7;Kkx3O=}Wz*|Jz$W-_e=v`np`dNY@<`)~s2xeBZwG^K%}aS5;IO2rBN9 z$~tyT?a)q%D_4w-4b!+X^}oKhZ&)o68nk2jili(1_SF8qc23sXZcqLE_faAjm*qyA z_xFkb5LbZgR8t*nEemA5ak^WP(9trPLV&dO@NgNI;~(Dc>2+c)qz ziHNrG$*E{1C@i`5XN#n<#>pw#;eOAOjP2@{IJ|iK^+@K?PiM~f{EIF*wvVgov`_u@ z%f)YJnXbOJW?j_PH5DHp#XQ`(IbGd<(yhs%tHWfC^K71`*xK4YTw9{7F1-8o`-a3`Df5TlwO74|M`y|J$ez5b9c|rx3?9Z#abN+k^EKlT)gPH!~cJO+4*I) zBvo5~URu1~V)xDBTidd;Q&Mz3Gkd@G_4Td%_GaegPfzvZ_XzkgFfb^zI7GNytpC&3 zJ)K9w#3PMQ#H#q!l*Nx1{n%kK@rTr*VEg*Cj@+y#XJ;AyJ@WDKnVu^}QG3f|jc+)g zY3IMPtp3lB``7xXhJOCZ$G0`>`ZSZ!ryr{S)!e&d`9=T#qNQ^jBDTlbW@Si8NP+koGk0L-q+VXJv}`K0`@6d#(zBnRpMSA&vdFa+k)I!>npMoPFYgPQS-`0q zy>-#tV=5Z_=G#qIhkM4YTCu9^-JP3NlX@%XS`}#ti;B9gtx5d8aF4@{%b^9izG^2M zgAD|vBqVgAA70tx%AIy<&z_2phw`5Fi2wUHNoZ!{gh~8wZ@jv?`ZM>>#tD3r-+p=7 z+|F}W+wNm(YUB6nr>3WSa~*9gNWPwT_eXHibBC>2SAX39IZ5C4R`}Ibq02W!t&iJV z6Yh2C(j~Js@!NA>hqRmf_&YnH{`ETnJCxz92eR+FY zQs(EKg#{POd1^VKycN^)%8RJ2~nh|yVyBQw9`#f^!4tD88z zRQ3g#B;JYeY@4{aRN88$?Tk5gyx+{kd?E`Hxeg!V{tCAEtyE z2wa+;dp>TjhP3hK^z_ubI|^$L?I^r+I^RO9?)?1Qelrj4_tP*IIAz1G5?|%!cI>gb zkHn|r@?Gz@uQ+icP}!Y@ZG-qFMg|56juzL6ohx4+a_!5#y;b`{%*2TkKR-LOuk^Fp zU)!7?9j$$RK9?0=oSl76*4m)>Yf3rF>|9q^cv~k{@JC&cgLng0T zqZ2-z_uaa6lO`SFXt^J=(V3n9*wIsU8h(zgA{^P*R+WC4pFE2zhUt;FpP!sn#e^r{ z3|yHOuFMQg7oT2rsXzOfwz_)y@o#VaPEYsrs;f}gGTXNL&JWJ6nD7JS`T@iFODiuA85r{|x! za%D;%_vQ~je*8GbyJVjBw>|$C1TFPCvqw`?)16N~Zd=O6B`t@9bk~-=y!7W}Rq6Y8 z@82K)Yn$}MgZ<#tV>PuE|M!)zpZCXm;vt6_;;u}JbBjz4?&6i6k-H>jZ}IWISnF~Z zH_n%u*Tsl>TduF4-`pTX9dpU*IF87_kZqWyQK^d<%w!8iv z+rNsR0bCf0=tk~ZbH9P{%;Ktp`S<0lN{nB$tz_dW02Cb3^Ra&DG}b)V7Q1M(^6ObE>}nSB-GDW5*vn zc;L=2*VfiH<@(ACt4dc#ZOtxPIK3Lx+OG z{rl}BgOZeVRWvm{ZB8%mjJM947-{(F(^KwsNw1`&OtY`Oy0f%U*^XJm#%O#~>XPM+KTBi0m@6!H%5>@Zt#lCwlFU_GHx~}GR-|{o7 zA5Hn#)O>3 z7VE_|#W=+EV}D)0fB1)RK=s^Blc!&QyrKO4iZD^_u$+5)TBmKhQankiaLa7VTB+{_ z3;#IGm^yXlq)D@;P4hbb>)P4bCgJbmZf%K_kh>IgIq%L6IRSwQbEihGjrwZ!^_9`} zpC?;CKUepiGvWF!m22zD-`!p>AADog&d`(;mG9-2y1Mh1pHDeg{rA__lh>}@+3B9~ z|3cg&{|R%Zgxr60f49u+?HP`au|GDRJ#k`zv5k&Sxx7@!sw*o3lOOgoU0)v?y(6LS z$n^v@b%9Hc3x(AT4Hu?(PW|-s^!eGwzkh5@{>1!nM&aYN>b{TcD^GkZC@(I(Ibr6< z_rVKF4qbN2$Tyzcx|e%*`T5UZS6^545?NkuUHWp(+OzjQowXPH#H_gV!jU5`R_rfw zt^{1)uwr&~#QYyNSM~Hu8I3_pYyN0DJUyUl-s%}&Bgijozr$py;pyIeLw%?rl)6|-lU!T z^ZbS2NpG%cSN0iB$a6k&q-I0brIzT6HSFzyidt;G!R7qJWYHNbLu-1e_94&8eZC{>h6uOkd@n!bc zRZ|)Dxzl@cP zz=uhb6jS|9g`5tvD4ZMd>Eh31`)sl8?e#edGS+o}=Is33Vtnb^*}2BQ-$v^RyfV*D z+nVsuX=b>fYU2dIYO9qm?myL@^NU~RgpZqV%E`a&SAINOH~)z=$JHxWV(!{nmApAI zzv0`?-K(`_7x(nsadFZ9`}q9+v@^4gwtPF@FK4M&QlYr7OIuyQ?(5gPyHbl|Vq&7A zX0^4sc@>@N7XP>N`03-*cM2P+sU6EZ)&r`LJF5+s?oG_e*>kRv`EsnmTzg+BGap}H z!IP@R=g-fv`}<>K{f_8|?n=H&bb&(2COZsULc zZ=QMm!515oXR2P<`D9DY&6C3FJL0;(eED+Bf6kBZ8y@<+S$@~);IX4OXI9S)S6I?h z{`1mNZ*l!g(-n0anH~n;U%qjpVG&c}VYlAx@9${7k$>@RZS)=1bkCCq9Gk<|#2lSd z9rD&t+5OVQMa7;fc4AZ3rJvc9Y-e4fBNcJw@vc`Bg*h~zot>L2FDB;p+#yfl%7q14 z0Y7Iy5J=n0$jBI+tLAC-a%uVPt=ao?pZldXvVVI&+rE5Vfhz+8gAhlHE1xOTiid}u zC|z0tUmSE_ib8KpIDMz&FP z`TMvR8ybH9z9{^j>ymec{NH_*o7H^oYQ*Lg6sF!XFSoT_X?ZmE&hLkh1x~G*vG?wp z+uQS=MH^n)R=Rpa`Fof9s|=VP>RA8ooUCTpy4-*6k}}K7#eY8tDvMd^J)0iTvN*Q( z>8I3*OpPDT@BbgSsYKHF;O_E;Z&}X1=99I)`0`*(($WmC*NZCDeqC%VXkMMY{(iM_ zv8tL%z`Y&i@AdW*oDf{L;HccPbwJFburcIDs$8dv)5zrr9(9|9QP$_4E?Cze~@bJM;3g zhNEHdsXxAnzqjT7iMC2LGBw>>_E{`=NAdAK)xPc>`(M^9-t6~mkGy@|lG?h*ze?Xd z%{{-n?NYMenQfb`i+%`v-~J)lJH{-hAa47#_(iK$c^#beQ%j&zU+B`V9X?aT6=HTI z{JUpc-SV`O_0T=r>Y&Qzwl@!iMYd$bu8-gQNc^3B#fHl6X@4f|={vgUs5cvr%n2Fp zlQ$=YD=aB)`1W((-s+j3j8ae8eEpox)ml0Ahu!z&x*m(#S5w|jIjO|J(6C2QK+N@u z#H)LY*^3MN|NXwdCv)-r&(9})TQ_Z*SaD&$RJvk3`<=yUjorJeXPf2z6uz^&dipJa z&d$y|-qycw3NLZzIC1989GjEBUQN9dR+n4+xmaB9)BV4no}N}>XP;@VQ2PGf;oQe= zb)G&xJZrmOulReM?a-;Vw(@uPgu~w4uy=30tHq`5c5O}6(}(ZW?rDe^}oJ$h={l`%~$&S`_r=0ZfOcjn71d~vsrdur0+wWU%kEktuq%c1f-qX z^z_uuUAwpE-EE7XF=4`m75kQI*q{F!pW$IIUHR|MO-(biFP2v?Tv$-Nta$qU9Yw#o zGy?Zjc-|8|ZTan6JHM<*=lL_@jq87i8^8SQ5xTeXaht~WXpzOU%WY-u}+_ioy~2%@82zEHcqf+f6?-Motbfylhe%qf-fB2+}rv1 z#l^)76%{A(bG%J?X0fmS|Gd|=QVa|V`k-d-<4H@I*?0mb|0}ipp1E|#4srcB7dN*( zbzgs-ntJ--rNt8-N&gPWmQJHh=&skCB-{0O8Mog%0H_?yZ8@A{0 z`8^s%ooy_8>JER2)hxb}bMC`LWi81+@3%ADn)&?PyxO0iQuj3mL`PSDy;XXqw!cIv z(0aDL&zzRGUnGN<`8}O}VcoJ${WN?RUc(c!OzqwggRvetKbKhaclqoKz z`(K}Yw)E1{s~X1)j(1Nhdwq=e9h6-_@VIi`< zcUeCU|Ng$k(m3ths;{LM?aTM=+cG`tqg(lelDf-FGB+P~@BbwJ>D5*3s1wp3b9;@> z`|y`=*4cTIKC~{$UGoTVTr-{yX(*C`}q3KwWyo( zA>u;%^G^NWlA0GbT9kh~lk!n~(;0^u*DViJzIm%GGi@^8yVuv(H{D}7dhCczMZy1r ze}7Ms|INU_Akyv-Arfk8Jlnk9@i$*i|AL;Lo}TOLd?pJ<12Qa*?VVGZuIhVZ(^gn79Zo< zn!mzR7gRp~Y2o}V1CG73P4vS1-nO|ct6@%z6Q6bD@UO~n427wy<#aVvb1rfR=l{;f?*Pv-}H zKXobmY{~7FcYc!o6 z?(#YrF)#Efe)E=3-bzJhdGMD96Bi#UI_i1OAJoe!ZZJ+<^i?D}`q{r+MXnEwjEv#y z;}-rGY`V86QEx3X1H%c>z)zs?hYJ@Mb1j?r}+pgG0u` z^@p2kXMcWv?pDO}`CIFv6xKvmG%upg>pX7ky!F;Oq(iReR@ zNB2eE+1DHqd3Sen_33A41s^`NHxSq+*xz=XXJ=u-|9_RmOO;w$CQkf#*1CMgq4@$F z$Ngs*BwSda71|>r-xaXLL19n*d%3w2{>U|0Zq2^FY;IcA)>SX8d0t!(FA8>u(}-DH z5aszZILIlosqER?=CUSD5VSo*U(W&h64&-*2{zixbbzfMP`XL<^#4|`AGBv(u3%FHcWqA&RidTYpW zXfpFl9w~Ei`StDf_OiX3H^da$-Md#ZW$_}VRJKEV&N1@U3bpgf8|M9~U}ayvH2=qG zas5l}-v44%7#J9oSPm(5|GPdvDcid0i$p0euj{h^KRzm(-n|nO`{(bUA5Xc(^kgiv zO!l`pJ^r(^`2F3z*5&`23@_RKi``XmvF@5r*YrE5v(58nhE08ScGChbO#v&ND3hco zE8ef1F^A*G_sdL)lb8AObIU!vy{GW8*K$$T*tvfkES~h+tNG0F$n5&RZjSfFTTf5h ze@v|YVp;U#gIoFc3l}EHOK{xYmhJvN%VYlA-{0TG8pONpU+fYe|9_ssO0hD9Em`+! z=6+^NJiq$qDRY5)yUV8^a!7Jw3f7Zbdb!y$j@7i}!h&}Go44~MO|!4gvP*7z;^;Wh z;@R1a+xOSM*OL`LIbmvGVBpTbH>;|vD}PUUe)ph7)sqv|<+gw0*bE9&f84LX==kto zS;P~Iq$eK#cKfuO@PwCtm$R)lY%P(xd{SqUC`WK`aHjmdfA2t@#jOPgYaY5f{d%Jl z|8UCtH$^tJzou+6SF(voQ#kWl=0SkM`k0-cYS`HmJ&)ux zJh|j#@QeGa($a1<9X#7|=^M4;&QJUHYqfftk`)j?0%WiLCrH{rvptdXaB5_w{^EW`6iYGkE#g86VGz zDx0s1FOJ%0^Z#VteNFA`85N?Tp$}g?;rzI%{?`3%xv!7?Ve`CxMd$OG`EOl6{MeX$ z$Do8&$zEVv=Iyd+du!)f1>M~kv$JOAt(Aw)1-fjq5>pa+s1Xo&daFa@gjw%PYG&#Q zs5Va6B(^tt-<~Tka*Mt>EOzVd|8y?YZy}o#m+O~_E0ykk2q@HRaTVECeOt2Q^#1HGmyUL`^S^ms zzQX%q?x!W5FJESAhpm&cN$E&AwYU0PfY9w-we0mXw`iZq{`Wre)XCswKC>0S`WTf9 ze)_c3+h}#kgA0jP+DpuzZvOG}>gPp=cv>Eb)=HYEo!PoN=y}=gvbW{$(tcmoy0Ee0 z;rsnLCm-FoaU-9jMW|$khWd50^=sGXFF0{N^!B#A*quo`&n%Z=WMHu1XmORjp3TbY zDfOI9aOkKY>`3oblR^t&V*D6%3+>0`xrdAphu z<(q!-O0Ae0COj?r+WPZ0=Y_vK>pfg^bMKWswbEby{rz2Y{p+t>gW}So_s_2n=P&zF z(Km6TpuP3Qb3B}!5B1)j;%`w?Tjo7oKfUPR=jZ2F#_AQz^fflKt&iC|OLI<|_18Bq zFJCz_MUo@`-VR4^;j-_yRu-pQ?K?Af+9U^$-Ba$R{>s~1`uUkmb#>Yn(Q`KnA3r;j zTwbw|?NFw1+6|w1hrWFANEfWVld*To#3PLclY=fySsDCu`Yo6Hf*f17Zq3|oGX34V zr@yvlv+gMQqxzjEM1`NjxYEU?<@2tstFNt@n)7wv-fGz+*K>Yl`hgSeNUm!12zOTK%{l`Sh)c-(sRp0h=%Fx)xq>KP4zu$4(S zRu)f>+L}FGJ6vylv}gRj3P)>Qiy5`E7#J8Fm>M^nSokgJ=BB4!Q>Og7Z)>Xh^zrff z-+vYybD6uVw7pm2;znn8-GixzbECKG#P6CCuO#!JZ`Rr?tCHO!_7(j5v!qk_$K*h> z+j~M+owmzjxOsL?;h!6ZGfl5wy{daq+S^LqTHVmF@%7aQznLB;#mCFnEo^OldeS(( zMRYOisXb?wE)I5d4b6(l&no5o{U9(VvB&wxh1Z;K9qKquA4QvFpX$AK)_v{ggk=~r9P{}_Nv`=5qoObAZ;4a{_kV^-JEZY4xS1oW@gJ`qui@hI$K(PH2>yb6TQ3b z^;8!pZ|~Db&&J+3nX-`W5chPw>uXfMdg!W7I45hpr@!x+c-Qw0+?y}&vlO^=$!y0i zL+8ErH9s2E`h9N3lq($% z?-(ER*|&&y$>pmp*FD!1bj;fI_0?Rv({>uKAHTi3KVN=g!C#3pZPIXor8!m2}qr}%j3VYWKX zt!85SXW#zqpZWK|zS_^vJx%knuB<5hcq%l_C|-X4uRVtMR~VXSKkJ!%>HC8~?Up9? z&k^Bqwt5m0A&bfk>;-~er@x(b_56HW;aYcvFWUf%lG=0{x00_$S-rae$7<1j4`5ASmJ*)DL1&+O#~ z-+3u>!`s&>y=;7`?myQm_ZLqZQ`Xbq==qvCH&zJds(n6jXLtF+D>+|<`};iFAN=Gl z`{K&AwD`^284GvSzrJ<0@T8Eft!-M8r$R_*=+Y;Ld!FkF@xQdYS@p>KrJ-D=yKYI$ znVw&|T2CBaJbiq+rNGLvx-|W`*!rwAw}Xqr*53MaD4(w-<<*wV;M}F}W-kpeJbdlj zsi~*WOc60wi-<0NaicT-hoSA?_xAq<^um^jYKQa58BK|wG;yNhP9eYLGhJni-%Zz) zm6f$JpA{Kdm|wX6_mn5Kf0C1v-lUzK@+Cl3sQ$qGAHTA)yzVypo}QCr+_{@Oq-omY z4H*~X>%H&JbC9#IaydTzcjuFKmP22@l-yFfb->xZFDNcGZQ=VBFSiBzf3CW@x8!Tq z*H=NzT~A7NGK7!$&Nv{`9c`YsW7m_qUU3tFK1u6!pYwCt&#w+&`>9IhtHj!E+rCZB zzJA)f-%oM>b2$bE20^AoL6yzry%#QA_?CO)wc?>@tCQDsqr-G2?{e~ZyP=w2-Y_{L zH8u5P{=La1hUazOp14>qWuALVf8n!#CmtS_yR-kizE0qxBQ2fRjML6O5^rm7zic$$ z_N?qq?}-i`+zZ8|RoPdzwzTHo+>?1fuB;<#rl@tvgNDb8T7QQ1Sm?~ypU=PB=cuTg zg2=bu`aI$^iTBYM$7vS8Ht&NpT0zO|}9`GK3Ka>pL+60H`NIdsQWQ|NuKp|ZP2)68}2<{e+= z+bd-*mbXqlQ4lVur#6dz<^zI?A zS1KyS&(8hMzG0BYu=hP@3ux_8*&~(xEoH2-uP-XQ8|)XkvL^EK(Ytd!b>2HU-{#zB z^+~DTVfv06H`L^6}%xUg;-%FAZysoOo%RK26ZZ zz{fIc+ub!c){8Sd*Nrf!>ba7;u~N1>ZvXdfGmU0iidC1rk9zVzL90Sx%FW(uZv6dM zwQu_6wHy{*rlNj1>HVF9qD~A9Cxn?2Cnl@xD_^(n%idz8!rLXsx2>JRV{Bx+Wy_Ye zYu2dw&Pz%;QdVeKIqB=p%{j&U{rA_seo{80?&l@d+nSSG%LB_^->ZFFm7exNJFDRS z?&|M9HYRV}X!vDX=gQ3GeseXC8znw_F;Thr&D$NbXNhpOq@3OL_3y9E8?_7+4&1$~ zYow&4uHN1~y)x8CW#8%U)e_~;-^$nj(_ABQb$?*(>GzTgR~s-TZq2$HbH_I8YSQta z3#x)ve>w6o`O1SUSFX%3DxEdau=+y&{avorMvix;oG}-eR4lH4ZQWV6RsAnt-n(~i z%c3t|zIaX53O2hS8goII^U{~7oULC&v+78dlo0_EWS^ROxNK7pJ zb0zaxLg77k_xn2vZS~ChiV{AH>CV`p8n~kI!eNkgw`pLz|hbMGp%+4ZV zbzdX%^~yp&<~huW?mZozb!E*-5j8cnb&*?*EG#UvwY$sKJ1+GqettH6-K?OpSNFGn z-@N>MieA`?q@!KmRn*n$&xB|2bi6K2-gAZ@N)lh(q(G zsi=EQXz0~%vQ`#rqt1S5@|NCIeEgg2?QO1I>_s_gsZ*_^mL)X&Q1{>S?cF_DQOTb_ zK@E&4Uw(agdpw6pw%hOR?ZU%6VkQFH@-C-+zgO#%7!`Hvn(plLb1kKXKQFBKC^VH< zyZptCz>1IY&vfrFFfi1B#+tW^Ki7@8u>U_pnvYr5tRpo)6gXR^$hVwL%HrCdcT>qy zvr}{VvXjdC?~9_Ix>QDNy_7k*y?w);nxCJh?Eb+o=vnjU$HZBMA}3dHs^%OCz9(%} zapCd$auZDnj^}4)o0rYGvLRo_Yt@!@bKbl$TN|YnVOXf3qy46~{o3yI@9&OY5*OlL zWx%BP_V7XHHoa)Y-OXHCzLz);>{sp2zt$sp{NK;-T8oq#H-x{x=RHktvgD;54Dmib zKi2J^fB%6a^X1CDJI|FzSY&<4csPaY5xdhZ=HGlWDOS^TCOGl7Tuwi;VWv^&j!SFK zoM}1YV!HLxtXZik%OWq9vpac-cTd;3C=#=O(Pkw}%bDCQA;k}G*}lIc`R)3Aqv0B=W$bKz0j1<2#SubKD(>*t@VcUI}0<4U#vNu zE$_E!Ud78ptS5tu?ac(bj(oSb)+rLXx6L=+Zg=_HsGUmNgOf~4zyACz{^i%-@`oEw zoIAB>y{w+VsXqss>UH)#lsuGeo-ZX4l5{FFKj2b#XXirJFf)@iF*`SX(0=r!Q@Hre zTO(uR)^1h?h6f6uQR8_tY)W6XbpGt0b52}R^5LUB4^JeP{m}7?i}e1I|9{cf;Nakt zeXEx*FMo5y^Iyl+pNqtPzMAUy``f?wingrGiN@O6T`ZNQ)7?8yoOx2uyeaT#*}Hqn zpRYOo6t%8?#$$R{WWM(b1181eN6)U!-u|wV)qdfeIddi-_rG!W*ZovRS0+W%-N*an z^V99E*B{&Nb1?t@Ex(y(?yzKJW?EL?+mm*7&6D%qxwTmlt5>Zun(q6fF+gRX?WMDK z4=)h9{XvwAhhv52j#*JF8ZX?rbLZ&Yy;-5gKSdYo{+%*;vDV%>?|E8EW~ljXnrT*B z)!28v`Sd5D{r=3qO<=Exl4?&juRXn@%^Ob29$1^eB->>!kKgLck`EksDi{6Ee z3x3JlY^Z+ChaYlAsx#H*M$3NQ9VSlZusd2)z`frC=AKvYmr23gz@#T$`mwl{vOPOYA z1sP7P=&)Iqbbp!a>1k8$oSOPOUcBl1yDGl_zu&))S>ti1an<2Pu2YIW@6w+1@z2f8 zH@}tX$M2aiMMH?gHD0`fcsn_&ZS9X5>Bcl?H zDZ+Ac|30Ib0vjwv4qki;9RacwF{G)zmb#Z|~j@?>^S>${B&Ctsb&> zo!fF}qvxSRKU&-S<#I1=nOUIt{M@{abB$EjZ`5n!m3Pcu8M=F0lYI1f>+)%Ok$Yqj?91;TyK!X6>HPhHt3&ES7GL`|@9u7XS(^{fzGm08i*d0z1qH^}FWOqV{oUQv zce$KVKOP_N|7ThB<%Yfc%z4HZvu4fu%f?d>u|s2hw%SfxL%+>Sxz@+N*RdDqI#T|0 zYsSSxXYVeavLNu>$=GwRuB!Xb+wr~OE%V9%X2tDGKZ{v>d3`_r8NarQl1{DB`=Z3ks zZo0mMOH<%kseV;el~wEN!|eP;`Y$7VHg1(Qsp$Cg>+(j6+E-V$t&N`H{$pRRjMFcd zEqQmVUT%8Y)w8VP{XN_8^ylsT@=vT)85kIt5(BRZvA-|HV6jKn>A}w`FXi|dwW-{ z;Pvxd_cQ)|eQmY-+oPLOi{z&+Y?&}|=EO;pe!Yu*(VKnUU&dZdQh(z5Z99Le%s%;N z()s!JmA2n*ZLXeP@aWF{L%Yl0U)kBcaN)vv^W@4U6`x;L&-$=6`>vPYQ6C>4UM)!v zEw{}9AP{QUhJ2% zwJCffAzS;f=TNM!Q2$BA;_&VxcU}2pf4P62rrX=wYieR*a(aqp+?t58va%vCubP+` zkAe^HVt2>wt5GbD+t7$CS^u%zdi?R@`I`Cf?rgNRvf5Sk_0{6z zJ*jg0tjfRf7(ZcfcVB+!g8lymXRiw_&h_{8T-e&u*7D}w-q%mN#YI+Y>^Cg_`TO_V z8{2yS@3=Byp>y=swDonD*9nxZ$WBbOta;+0CSX_hWrErKyO-R0H4IF$uIy-9x2|nh zRalvs>7$ns0Su3~rGA*8xc2szr{|sIubvjyFJgWXC{a*YoIcGndQ0u?Z9P1a^JUp` zf5abdi;OfiUlFo0=H{j?_Ai;8s;#ZHU&ls7R7A|!JzIPK@pDa=u3nb5P2;(Ic)7;* z`zyQuFiKb$l}jz0AiwB<$BV65R~Hp8D|>s(%F1eI|Gkx~gPG2pI_1;hU-94W*Q=?k zx98vQnov4vvf<-n*Vo6-k9V4xdwknkMn*=X<>rQllBQ`o#g)DLBPNoncBUj?-iQ&R3M40g8?3+-7oMK{_nIQa8siCXFQz18lS#_N+ExtGt4+20r< z7WzSE0l#Lgty%uXO+`<=M73XC_UfLl;iw*(=EnB6aeMy#x{Ck*UR_;%ef>sfUPcCn zC#;Ph{WV_G5{>J$wD(~ZWVL0mz312M-CA-*4ExK9GVx78a5v~?dZO$YoegOz%wIS1+e676@{iEYFddFFx__VvAdo zr~E%XP4}~@K-$Gh=lDF4{JPSsB2LGZzL#roJ-^gwpsTw!px?EXOH?at%?a+7nJ-^v zDk>`ekN+mkz#t+!WcOV)97?^qXXmtOX8TnvHDBJ_yLs^$?fr>@?c3$mP7w{REddMSy}y=bmhvG>+9pyRn^4qd3B3whuyvt*;j5V*|>Q9`h0Y)e2pKbJA*gQ zL5xD<|8Kvv)H{4_RA{Vo^FNKSH4$lPXc{Vm>g8@;5PA8a5H zwkl+$Q!AI$tXWe&+SUJ>Gi}UF`etH0mh>&(|& zX%Fr-K});V&70@tIkjKj{@0I+;G->*SFTv~>gB7j3s;xUTD@Y$vZYJa)YXq4J*K9j zQggcsw6*ZTb7uveS6b3ivu4c-TbXoaWpR3JY;11s+oYr2!KbaYlpbxo{y4mG{k=VP zu8hr{=682}*NEFA@z*fFv3cJWp+R0N12?N3?NRfc zeRWmn>ZGGxQ(QN1-eh0%LqJfj?B}VcKX&X8|MxG1_g_rvscE@4HY{AWEUi;W)xB3T zIWh5Iz4?!~w;vzx3lv(sf90m<=jT2>C2gMRbo#mf%&AkqUb#N~n(7_t*t*}nm(=#F zXll+hQ}ms%z-xNg>e$_7y|YYbndc`)EbS_*%r!Or`eezI8#iXWc=2L&__~(8)z9C* zSC45e`}s*XYRiM^7xsOvdn79>8@i|BW7O6x^PD>?+bE;YluXr$+?2ij-QweYOC2JX`ADAPKa{y*=l@rs z4<7_S+HuHQ&By?ho+x&c4^gdUnmMV1+3=C-~1V(r63Z zUH|ivsz`#$QxU&AHa0!IzMo?CU3H9wli$6+`}^D0yt{4x^Sp{aeaYN*eO;XBxfk*R zE0!%grq9X4vti@5v_lcM%idOfe|dXNq;V}X^UUA6>gwuBHh&I)wkW*u_w{|c|LUgH z)qC0bK6u;J|MOYBqvGM8j)}_u;=GF2088=VPe6FIVCU<9l{pn}EK2M&lj$VIn#>U4wucM`;!WOn1+vCmc z>F2ep`un?Yxwkb8xYNs{wpLwuaPZHh2@f9VMw`73yD&E)_w4Gf4hiU_Sb(KAs{P^7e`_W$1A;Mmx`o|l((i|HGhnT4e+6$!t#E^=@A z_q=zit|I3By>4!9pPmbOvLC8t=i8Q{`PCsdE9+EA^zGH*DMvcR8`o!?|KJ_Bzshyy zx390SPx6(pNqN!K$}PImeqn_7&ew;2ex5YREcf=iTbrk^e0%5S=CiA21~N?EtkKo_ z``f$7ok^}~X=?jp(iHX-zrMCW(OKj3>Bt9rtG~PTOPtg3RoUlYp7mu$=VZ0OlIi

rDKk6oqv?C<@)9#o#xzf5anj1qN`1PqFnFd>>ZjQJ zu&`_Y_Ez)D+kMe8Fu30dE=|H66?7K4^~kucbUSFGaVoby{oSpN#pmDMoj6f&Q9f_W z-Rf^!Hg7jK3ew;$5)u}6=aoNJDjF=pl=z!Z_S(AgFcnQt7Z;Z=?=MFewXZtkAR!^K zAlfl}xfAQp?q7d?e!iwVTT}QGcWa}8sp-}!t5$Wb%Gy_2Ji|W!{=L0EiSO-d=Y*}* z3tbhmZ?pG>&W%M!KlRJodwI1qeh6MC6B=5!agxK0Es@60dG&s=v2m@N@~!Ru=jVTa zeVO+Eh$g6p`s2jW;u_e@#{1mT!X)R$28ZIOPoDV9wt9N!=AHD<`*a2FecO{AvDC{k zyZY0SkCK;{1!ie+HGY`?SY662E98RjjeWVVH>rB>*x%Z?VS!con*;k>LnSywzdz}` z9^`&}`JU>k)e@3BD=V(7PFGCVTexP;nv;{)tdH5JqbbOCXpaZe!c+CR6?`lX1=L zTIuQLc5HL{`M6)d(^Jzg7tJ!uPD@KWw#LN7BxUjL-xCkF^YX6lk+$ErXkXpOM@MyZ zbV|Q|`7v2lRgH~}tw{Dp&A&gHkA8fN`N_}IqNSy!AHO$E?qqj=w{`u$J4?I8ot>Rt zUs?OR`u)8vqM)`z=Ddo^i%vV%1wUawG-b+^@bz&|Q_^3DUJ|lXGS`JGg)Tx3_wsqGD$! z=azM4Z*O1Tm>~A`+1J;4bFIAMPI0!(x3BjT3)YO>l(Vy_w(h;_&u!`J!@4R&Ra{u4rl7~&KN*^ER zPC2eIBe$UR?TwYj^+xjf`CMlH^Pk;|`2}8E?QZm=v2lZ2?<{fF;OVQNnq_lk>u0T4p%KM#H9*B&;McDoJ9h8<`O{XM{ZLo!@2}EkH7%@U%;?7Whf*4NiRTj^wV=lc5C**2L$ZpZeQSKC!R zigo;T?&_K~Yc?)+x34L%f1SO4$rk(ee;?EP=h@D5{rcwG+oz}NcehUz<_KRO|Qyt!64-KvB|WM}+iXFF8( z_V)W5TW9~*3$5d2b>d$cEv8xW_U88a_np(cyt+Q`+FDn)b8)~EQI7k2DlhL&=l_>n zA<}rPPujlvT}+x6mq&1pTI{}(&T36#fq7PyO_vffJXS1eDZ4c(_4Len_O6fe@-vu~ zm5StfmukHJ@%365-_f%+fU4IV6T9@lZZ;H6F`qjtD%U_vi9ME651T+f^K0HFimZQb> zB3tWPRd*2R;|L)Bb&&evP`?UnZR>%GQ6k4|X(c{PZv3m-hUCGp6e|!?-wbQR( zOnGoO((CDz)*U&@#sbThFV`=ODfw|haal}o@aJ!Ls}I@bXe`OSziaC6=rn~l_m}tg z_nQZ2MBe{&WTmlBVnBdEZbE_s!{6G!&vb=@ga7S*@8tY4R_~P2G`;wL$L9y_=ex6~ z@b0l&v$nZTaaY(={9DY|;p2>WkLL?FcV~lARCVF*zt>t2b<5%Hv9Ser{VPXEh79aMv`GGgI0&-Zq7=Z z%A+;)@}*TFCl9QND$UK!{b%B;E)c}QStC5tRzo_ywxYxJ$X?)=LNzORezRy6BW*!J7-t=Y02F)l4puEzFac9l$UtQxv4od zk7@0c%9<%5rswvr2A}Bhe_o7v4X`i3zXWU=d{O6>Rk&y;REPQq3mmmvYN9KjQ z+S*!MQy07OT9>>^$%}KJxRJNZPeJ8%Y)t+CzvVw|*95g{rrzCE`uWz?)ph6RtE;Gd zDBgO;VMoEiruu)DzXEG=dmT4+9TE+$_;`r*HRu0tvy^#SHg4SLD}C~vQ`nBiNe&*4 z(;uySq&$&huV{P@k3jr2VYWjzX3VhNUA}YYE~Da5Gn10fA2w`R^Z)E?Ro&UquC8u% z=Y>GUtY}I;tE#RZzpv!t;j^|XEqgP97dYHFbL!Rm{q2!kGXnS3 zR32R&wS*~gZPeCl`30b1Mno#I2C1^HU!Hq= z*U`$mR_`9}Qj+Fa9b&la>C2TTo;`W`_xJZLE7arqT{kf;O!*;@w(i!mXFD4Me%(CU zBxRLx;cz?uzI0AdkOfExaCDoTGFuz9eO>hSxaE4WtF#I_)hsP{E)CthWs8YtXH(;b zq@!KZF?VN~Wvj)gJ>C0ciHg_km-6;%M~(-^Ec|64{O5+ljA^s1&X%n@j1&toc(l6_pE4@9v-8`%leSK(DCa$MY8xOJ;8G;gj1_ zbTw;Pz(3u^_r*9e*R$>8Oj_38*Y)Y+$BFw(|IfCts`|Zc?ZiiX-lF_1ZEc}U`aeH5`pw;SI&94Y|Ide+ z6T`x;T{&qsT`w+WSx9j3=W59VF<&N2a#*IGo@Q#kesQql@_DX%m-((evnurT>iKfF zT2j5Iy}7Z^cZ$1$P3fnW)!p4G8IJpodQboR_4|FFhXV{n)X0^*|H@bZU?mE_EaFPOmb|wy{+{0r={Mi`l&33jvSeJdAVU@!GQyg ztFLXaUHsVlOzW&jhONc}K_peRUH*3^#R&R)Tv?JqrpR7+}|Gd31 zmCk31e>4UJ2M7Dc&i$|Vc1y@T1uoO9Yj5r@=YQ&-D6YB1`B;yX=Ber%oU8sUY-XQP zbk-;}ulkC1^mNqKv(UE!fN0+)Vn zJ9u?%b-$dC$op$cy%QGnB&uiZEs6Tnob6XumXovQ%v(@Ps3D#kWuZ>5-h;~LeXrYi z|4h2H^s1PCtX0AThqQ0ZzrUTG|9$_$ORbF)Hl5wFGI)K=z9afuHqW;`cxzVVl*+SG zKIfOJdQFMgcBoU*CibAii}&yMS6w}I+dSVWJ92kk?frj$XC7K$o^Mq?!Eq!s*MxEvW!!^LO(zIQ(il-!g_uD{D~hY#%JUt6@C51`|rS)C!N!;ANcpzrIO$0 zznR91@AGVbl=lV&Ntq`yy|qd^Rl~mQ|1$@RxL-0*xM;Qaz-f+ z9w`2qw0z&b?CWd%#P0{LsZdY}Uhez*_xr@BOWRl8*_(a+($T&{uj@)Umfi9Te#qND zaq7&U$0rNRam%*sz9 z(YK%JMsZkoK8#l^iP}1gbG7Ellc%TY*%n1)IsANOniVHxv!LR~g@cxlvYv=?bgu93 zZ=XGT|Ni)M%l&tWmi*>@;cw{aT9x1+^x>0g=*zdax8FG}FTrtASUx`YrCoxiaFoBq z`^w|Nld8Xd`EqM(zJ2Pd+@+_c{tSM1Z|mj4hfeR_GqE~(J@(TMFa7t{yzb%getGl! zTRTcWr=8rIZECtzOgCCDa$8J%{CxYTkKOy(_+=D|t3eIE2iKhxbc%XEt&N_p zxxeD!p}yQ`XJ==$#UFj&Z7V04x@w+0{q>x5)%UljeLg=t+`e=3+sQ)0moHy#Zfe@J zX%l4mwXw0J>fWc`(}Vtj_Ves4dfL;|lYV~g=_QU6*Eo1wTN8cXzu;~^)7ss;qf=a1 zQfH+5&o64-|NZ6Wvb$9elk97MeRcoTTb~E_%a)`u~ixF4-;!IBfOoFuSM1ny|IAX3df^&iC^2dOb~d(&R}h zs>#QD=NsqGE6|mcl%Fg#`Ox9R&(F^M{3(|$R?pwaekiOlcw9737BQxD!E(r{8U@pnVym*8uOMe zUmj?XJng{39XD3iR&)KobGnJmNmh35<>%)lB|?POeNV31Wg(EYDm%l#dfJ}TYyZ3K ze||nyPYmSwI$f4Si#jIVd$KZE-6uu<)nt7mqeDk4nTxAaR)lCq|0XTC@pU%6`fDOPH~(Ye>xckbR5^iK37SBr^bLBWP?Gyb?< z6xx<|b5GDN@oVzZ9G?6CF;BC$ri`ySuNjJfEJOk)ad) z@Jsdkdq&SEu{cfq5q5R)@xGU@UY(5e&dE9RGI%-YxD5R%k{q40kGnlo?wb^;(m3J8 zt5@&t)-P~7JF6&c?&Qgv)2?dWetG#RzmNaL=}li+I)$I=biKd4+&|rKew;yl+^y?> z)r~cah*PE)~zoy zH@o#qPJTZnFffq$*SFrTt|>p)_4&1ao~~!O_e#N!#tAd7uQa}VZe8r|Z${Id8`Hba zE@}DE((K;%;q+&&meN<(N=v^R7#a%db#b=H%zW(cv18}19TwL1pX(Lv1&TCudZq2- zEY#SQt*mavv)YUU*d}5uI&G~A7e)@TO`j#tK_~q?d+S~2xzPxxd^YWy94xca8 zA81&2cUSnjn4NRNb_Ol=nywM}==1aQQiQHeSY)z{hei!8@1GHYSGhEQ^SI_$29 z)6O3HtEMh6O(*ix_j`LagoRU^xOl<-|B~A1g-)$^zIM6G zo;h=(qqCEfmv?W~(^D^l)j!Abfg-3NlC2<8`|s0C?w0rW_lMUg zPTBP3tM;_Zm$X7xiHLJNPD*xma#E4)^|%nF_3Cv!Z%gLGhk}ujGmWONQ@$OIES6%ZJCnmm6lSo1hj8&itdVqO(CJ7+Tmt8;p^5|2#78JdyMta_Jap2b%XsB zp2S^RRB~#%IJ=W?p+b+JhsT63n!i4;T(LqzLSl;c@}!h__WD}Paq9J%k%C&_4E5P$=`B*n5Cs{MZy2@ zP;mET!2|^XCJ)WHJr$|W&UGi3`_F%rlx%FgzxDwyC`20;4Ta+?|H@Z?XT0ZDX;JrQ z+qUwu6D>RnIUKv+T-_ATEfRIh>2!bSsaHu;e1E5lcuD9k4fqz9dfw<1p8(U%#y1a_ z>fZgee|fRY@r|;og*leL>KLA@%v}=RH{b5xx4pZkgM+=n$Q2xz4l|X&r*tmx5d@#^ z8L*54e2}L`Fbg;mrf4<^fI{t*h$Hw|&(yA=rF}zwaq7`f-ten3wofwJmSn73wYy8} zq*p#auk;jc%f2^%v%R*hoF#el)akyp5ti>keqLa~k{JJVMVoAorCS8cLKUtH=e)P+ z?(F-%{;`Hp=mx{$*!TarCx}E&3VEOX@IR|S?TQ(Zs?$R@7tYjSVPJ3&)G(+hL_&LCqYqm zXLCT-?{X9Prn@P(WsQfuJO7Eauyd-Sr{BC)TsY;#rXM#8nnWip()!smFDA;OLxh2W z;e~|bjr1K|S#DWx0=7P}@>&*ovb<>9jMGa3EGJse;1RReT9?+LGbJ|K`)c;hiu3)T zG5K01#~bDrt0u0OdZpDh$6NE}6|MA+z=n*g%lKnG3zbCI35IRTwA~m{IKzjHfx)4g zB~dbrtGuTkS6Bv1%UPwDpzE!!Df?(W9Kl?j(eGn`Nx5y1BJFF@u4D zK_ihRabg0ATfC8o}UMQZD}sjk1Xp?8IJSJq26+nsBUyR$Jc zG)!_6m}U|l_&L;BR(HV?DN*CykM8jWPf*fH|Fo)p+j*zs9M%)HbT@{aH!18fWME*3 z5^PcR3Hx->$~t0Yim;d7+x6;!6$f(vIrT0ZL)B6 zhTo2$ZNAF;Jo0{CWMW`&_^HIU+us)QXQvYy18Ex2wrH7BSnXBqp&wNbM!TrIJVT77(a)=n1tSOx}$ zr#%WfrbWx{XIRb-D)PK(-#9_RWqYJxOIOe1i1w#ErP zlX9kfetBqPt=|6ii+``-aEdHU+Ulj3t1?~e-Sc!F1_n@Bwsdi7Z|9eWjai%GPNvV0 zbNN1*`@em&iuFdTqvs^gc8A*>=$B+*aIjS3@NT)YYpSG$pYN5^_f*|Dol4Aa{`Z{x zHe&6g8*Z1IMEO4?voSCP^l=;#+PJXk!aB(mUdhL7%J-^gSf1P#b?^MPCpkf}HtwFs zuRs5K@py?;i;yn|14BR?#~~rb?f+&y+aY&nj$DK!;bT=U?!Kl{Fl zU{AAk=XZ-7mf707WL1)LX4EP@?Ww94W6y42=P5rdniL)TYwo2zIdkJznb;>(tBSQ8 zZsWCo^YifRv_a%VuK8|rj?2I2O!@I~{;qrOv+dXZ`af66 zD6RHhM(?yg#oP7^{&{fo)a~;(7rYO(DL!%INWZ@EaX*pU7uEB6Y^qON?GMSUJMirm zYvSjo(E5G5c5=G+N^LzV7VS5~Ao0zPLf)@m=huF_d1Ynra-W%#&TiXsf9>{vxsT3S zclXx+`~2?qzi->$)qdyWl`u$Hy>{EZwA;DId!?7Zt_lvmynKGmr=Q~U{r|l&zMk4A z!oV;^7VO~vcM2RcYQFZ_7e8e?^z4J~TSsNp$dB?|`-}X)Ow3L>9W(#?tEkLxK{r_+ zc5(Nv<8fN~wm)LK=){b*i~Fp8T{z4qZC#dg`?qU@?e90n^?QG&{eKo39$R~$k=-j! z^4_QQ)$d-eD}R6HNT;y>|EKGZOyD`3u{XFRAvyW*-SYc?-x&L^f2;E}b$Yy<0aMcB z{`x=7sgc=tzB8E=fb87+g9aS8-I9t`rqGmiuYqRz(^ zX7#sz?Fql`wRt*+)BAnA2A(_K*ZuCi5zKx30@t$4tXXQ?&-i;ykF9$7`MkZlS=Q@U zes({n6By?x(f84H6HvWUt$scc7u~zxerWZ^PrOzh1Kc7nmdRf7bgwzvC1h zus(dZ`|YgLPj{s+U|?Xd?NE4Ax>vYmUt72QmuQbopYAQ5zxUPs{Jt3zZs+}f!^duU zwfT-igx~S4t;Sz6mF<4KL6uJi5X_kxm71`SpCa< ze&KB4KFd<)Bk5D3&i$ROZ+0mEj(E-gmxm%mvro+U5~m(<*lc@tyT#PMQ;r=yw&wg5 zS7YYM@t2?b3GRM!+|Ng@GOu0U;MWn}V_*1D&L_KKZW3 zC2>dD*ZKaR(ld^pI66CR-*@Hg$seD6&^;n?g#CJYozRazydMvyR@*LLSf#;{Kieep zxNNzQ^!(aiFSlNgGp_#jrof`+%f;h!Dz8O$E!y+tQg?VKwQJU_S@FT|LCm(C znLBR&`jU9KEi^Rr=;M!1Pfu5$d{WW5tt0*E=bx(H(>mN=zxvhc`qI_#_|5+r**kh&ls=v@K7Xh3_>ves`>HQ5KI`vDn4P;#bfZ?> z!!G;Sf4^R@7r(biPlT&;{+FMh!{_luMMrDfwMw`J2Ty)+d)BO3?Q(TT=I8u*$lrhd zc~Q5RZqhsf28J)XEQ#wU-~M}Zvf~V^2Uni-|9g-he&^@wfQ3z;X1^D+f9kdS|A(vb zzEk4fEL`22fA4#~Rp{Tz?Q`e;y!Ua^qHVW>TY|rAeC_dN%jmo$8 z&O1>QxNsx;`k4LN45cD1LEq&~f6W(RHMxQiWghL<4UhCo1Le&?S94IuiW~39CGdyJof$j`@493r~U7n zEB{@Yui+oKc~Q~P*Y^vLzinpcwR(7D^06Mt$NtslW*FWrjQN-{pf|iFcjPyUw%C&F@3}kr{uYuF5{2aaer`pZ)5?{1TPF(vp9U*7HhZ}{}Ie(=gGuXe{4KAp-gTe9KCj+f77zuW(QUn4VnT;bEu=-jPSPuLeO ze{7MYJk`rhDX`FZ^4T{#f4@t5ymI+`Nuwj&hg@s~S9e5CdpdpI>s{q{E1$=f-`)93 zdTz<3{_~dKV@_IkZY=!&_gnV&7Yp0vs!lGQ-pTthX!n~pXJ_vJap|;@@6lu5_iaz# zWl;1{?0QV~+qmktx^^d2r~fJZu;Ab#{=bi1S?#6H+dhBOZ~t$H^ghtiQrlLAH?eQC z174j}nDVsE-X^i{)~(wnn-_&{sl300?@wjg|M@@q+vE3We>s;NC%`8$)8d&^@fp54 zVYhj^EPrh&51jDp^!(4l-m|}#KAro#Gv_m3xs27imv5}+p0YCh&{aQ2{?~&_bs5pU zTi&eK-LKJY7;@*c^~ck@bDlkk*>yNkz3+eSuLbcH>utWDK2f|~@_x+&)`tr&`aZw= z*#B4O3=!scIqP!1HdeCNf0Mpeak2T{fg=)jl`B8>#qWMN!?a@ala1oD7H|J*_rCsJ z&z=`sTjuz7Zrs}E82c;tO{4wG;^ikg*_NjjT#lR^6<70+$B?b`WK4c&Kc9VqnP&8} zDAvT!=hvNoSJ$&+rHRg}AJ6ldgYSve`}(cf_HmB&TEV!ESC{Qq%_`X@6u(c*o@I4J zvcR_e8}s~ree6GP^*U!_vC^ekvuE$0XSH?X%EEJREcr8KeP%SHEP{`2j!uB>?6XT5LLDz8@>9MkpV{~h@IT|Z{aha1WLx2s;Sb=|BR zVer4>Fn@fl&GY&72S3_>;k#4&esB4C%inLx`uQY{zN}n6|J(MzOfks<=VlltU%t3_ z{r}JLf5i5_IK=I1Y+w253Ey*dMg|5iF~=Lbt8_VHZ72LXD7o_Rxlj37pZ5!{P5Le^ zX8-lP-_q~Z8~%O$ZF+tFjbB#NG7bf=JJEkXR4Zm}$d<0C{Rux!*Z*`kdsV#e!=;R$ zfArig>0Xb?ocl|7p@yAb`su%-_8Urftnm5NQ5@V}?YhuJPb~RDdflu0_iwVttX9bR zbyaug+POMeJNw`7e*f*Z`cL!Q ztPgXy``#*4&)561(OrJ$yIrsU)Vf5>t2w0^er7kWj~+Z|NoEw$g%nB_y3#dE*DqZ{=fFaLi;Q7e?R<~X`H^!kmG*c zYu(Q7Qw$6Z`}!5$#M(-^+|Zq-|gPXnEZ0 z`@+AkzdxLBcjMdAN$zv!TYcf#xKMY7T;-=@57hOK`tQ}-e0<}-#gEQCxXqv4{d`5Q zx6AUAHSX~Sgi(_5q?9vptYwetRo zv))@}TEoyS(crhr%f7P}pO_`%QmZOIA6`|_q$Uluqv-&*&j z;O^GzcE8_j-WjtkR$$+cN2j-z#wcT&9w_N0r?(*(?9<~2_o)|u_F0T}qwblM`Q#-f(^t&RL z&za^Oyrnn(cYTLW(=^A;)8eO09t?`EAxwSD@b90+7NSSEu zeSh-XgR@+|FIX2YkyJ|k>%N=M$#BCJ!RIq(=iPty^i$SnqtbsRh8qru=T|)cAzC8a zQuejsOP&2jx7=LurT{ILh4r36nTJ^xnx^=CUZ1zIE$3RAz_jw9M~fnV_4qr#x4Ct5 zNyf!0J&xK%Tif{M4KqG0*tKidVLt1cZryDH<#nIGUf+JN>NUIkpY^c<+rCYWPyMIe zQGMiZkh|lL$CLejJ)6D$@7+JDyw==@itB6db*fhx-wR$=^YiljKTGZ|P73+{=XG3t z_4U{?GvCRek*m}ug*UFYvMohcdb4%+Z$33mGd=iK)60eBa>i*jI@^xUJ$d-NpzKrI zS?4=SIBWj7-JX6buiW8tc8*MZ1=Cn6Q2FaUm(1$KzV9VIZNW7C8v#l zuM=vS6uywT*jjGV-G>R;A2Q5?uD&Y{{qy(Fga7lU=5Ah{pAg=~dFY$@wQ1{K3#(V% zzZ7Hl|6;rRw%Qks?DBVVHXC-jJX*PY-mlkZ&9`zNVwbDQ*nD=6#J+#KUdz>f5&W;o zE>rLzG(29nRHQ|HZpEW5nfHI}E|qQB{eIu?Uh_)!>0LLhKOPZ&b|$X!Y4pT3>GLYD z_4`>kYOSd&JaLfSGgnWm_=6+6Ui`bc8znnkTm;^2I=xPo?V;T6_t(m9@_=$bDC}ir z^tEz#D3r{3G1>gp<>)%=e9l8pKmXy#yjl5v@*eM)s`oF${`_saQ0>2^cI6et{XTNl z3qvA~+Wq>Xdys$L$M@x58&;@gAMnUNk@jrH+@k3BcQ5SZ?^fp=cQZh3omWC za=ZT6snr2y|NrRUyxaF_dvx=L)F+|LZw@YA+qn2hxv9F_y?o*Sb-C}R96CAqe(B$X z*4Kp{C3x84Dz8SL)t`TUe#s-wZZX}g+7}buQ$!NPd3iWk#ha14F|lH-T@9=J6itn>IH?-T32$ADQM`V!Xb*j9%n__r|XY ztFt~%UVr9Y<(!ZmLEc{;X7^_P&;DMLKk@r5#b+OMA1{x2y8fC#)$4^P>hr>%SF@u_d^ys@{Q=ihrt)yqXsk1aF)H|_St*X>Hp8&W?gMP{?j zc0O+Q|5I9h!maJCYodO|-d5x%C`l~oG+_-F3j zdvnRTrVV%5MNBnj1>~HV*<$|Q>goQqi?jY+)!msJJgH|8X`KH?7IMyi7NC(~|i8b;-y2zMi*_nXe$C_y4kJ zyG&6>NW_ePN5$iJygp}bJoC(rjgQY+KA)qj?jw-gcl`16__CX~^Q)JZU%eVGUvQu? z>G9U}nFh z_p)SXEXnw~zxs&$jBN?Kwx2n6Xy&4=LhA7o<=@9nzcl05x)=VrH_InIescWHjUTtC zWe2*=+hh3Yk4pYa`$)s~{EHO_Yx8zEMcHy#|7^H->g)bLt3x6p^nctxuJx~W<#*k+ zD^9;F2>7(({tx&0UfN9?9yZl`R|RP4%n+>o^-T1%_lb|e>%ZzxUaTUu_p0pv%KhE@ zkL!JP5#Vx{t9&B)zdS89b!q)lvFoKzMZ*n*3QIqqHMjkDBl%KRxTNTBnfgEc{dUh@ z2r9e%vVLB)v;1CV`uy5&g8XaK1^8sGzWn*De`D?*spVTzt?=fU!8wP~~rcdFGuIzk=7rx58 z61SrrUu3!Ey{>&^zxUXB5zDy-%W4-KdHwd^;WL85cLKI-m>gXE^Wm@U@tq%{%Ral; zNu7LI@!+#L)BOGK);`?+w{d6g_o*h%E)HKFW*^G8*AajA{m8k0g0nZx6yLg7cgDi= zzf8Vdb6(A_y#439lgav9+;TKUuQ6On_YD(2yM?_^$nizU);~MS1Y1_~pPBJ%-kiL1 z{r62bU+3T3*!k`Er?sm*)4LmYHWqw-Y5r{6-FPh>`xScg*6Plau6fT{d+++`yquX# zp|hq*#qVFQH}7%CSMlTx>^&Jf>#r)r{AF&7+a3PD4Ai4Mn||k&e63bwTEeONqbpqA z-Eux=adGN$j$e)u^(TX`omk7IKS}s7U(KJUNm)kjIa6;wSE+x_TXAB=mARloviwfw z^2h!5b&rnBy?t+g%E?LfcR!y$E?=|{&;?q~Ji@0QQ^|MzF<^u6<=11D9#d-(LuWPe-TSAIEjDh_d8UcSBM`_%B* z+Jxzj8az@aKTa&RSriieUc~?Buh%QD$JhV;l6m<^+3UZ@Wy}9$E}wsI*YnNi?W}Wd zs72@R^);Hg=gTDTKb=Q()qO0U%}D-!uljw#jonu++)SVQ^WX2+cV00tFzjtmc%!>i zqD9Wt_wE17S0?Vy+dJ>M7QdR5lK(x4y2o|@)5@01tMu=_G5=Kd`9i0Bdzso-YD+6; z*1JF2(QW*D?xR^OnJJ)B%z1J;I`s?1uMsDHNzcH=u_9l&*>FqsTjZ66~ z&)!O@IeoKdHJ?wk$>vY%?stTLcz@zeS;?RD>)Y2IP^>)O_YX82^Rm{@N475Sb!NNv zyu9r0wBO;3x#BI4n3wTeT;2by_dny_muLIq3z{cBnYjO(T-4M_Ia7)dTbuQ%+62JSKD5kIWgIsHGJ={*HNz)D&(l1D){r`B92)9QzB}CuQZ`HU7L;ei`4d)^Oij zr$V1SDT$f?IrZpW@s+&X(eF*#^JiK;DJj0=vV76G`hySJ&zFm}tP|__|44tQ?)MMs zzm_=JvD#IK##$&>{&~N|)OSi}W9KF>*{WrhS+B3Mi&ec|_`v=A`a6~qxmMYeOmn#3 zH{5U%bDOtI^VgU7*hsORv+q9TSiUL#^yST(wZGJVt?_p;@LtWj@JGW*`G4nTdwtc< zdS%tB@Wsz%%WvLnoA-Gix0;Gy{nh&D{h`jQY2|iT_Ah@bqS=&l##&;BsV^4W~!bNe4Sg@!&qJG=b<-~V|z88QY13w9LW z*pT?>qUYqi-M{7kznI#{$iR@ZhU3t^OO*oOF5;eZ8>!_i0L`SAtbLTq4&%sMG zJq^znF((#J{&?-e**UWgCPn+1t~@Qi;`H;^o9(Bs{j+MmvzdFYa?^!%FVgvCG`qZ` zzpb5=aphU<=0|_FPp!6ExMJ(ciu{S?f|-3w^?qK<*v#f^?YHHW)V`&r2`$2_CmMU! z-n;&5j=qb}tiyX--?&V9wb^cR@ym5~)o%r|Z&c(bG`n?P6Z*QsOy?8ZvB;k-+NQ5@z9GfJylraLF`yL{eS3kK64 zC%Y$IHqtT_dcRw0^+vYdNhz+eX8tToIGncDU9F22*KRV?EF1HuSMto znhsiJ=wPnIQSH6lamHF%)u)rZ^i+5AId80jaub+eqGtZDU&!sGyGo| znl@}Nk!tyQgTb9!<7L{-bHXhz(&_|OwF$4jJWD(?ZPi~s&=LR#OGS=q@8^yZt2Qkz z_1biB_c|WOj(4FPPVW4c8*d%B6xo%s{eB~;Yo(#il9>O}gyX3G>z}6%2I;L$o5kT& zve`-C*265%t4+Q4OFvw86<}gum?8|ZdflQ3Cfp63NnlIS*Uf?_CB5-P*ukfPZBG={wnCvcEAQ->qnGo1{GnGMs#C^H@e~xa( zv0o>*EnPBOHUF(?owfV+397DzheFq_k@(o!mAw7`#aC}zUotWKk_fKipU$~_JR?OpgqkES{?}gu4ty*Uz>{U0$7;ZLE zyw-1StC1C8vT$C+y41(ob#p+=EE+a}n%t%=3m3ood8%>J>#FW6)$DzrtgWs^CRQ0- zi$80U`BLrZMvzj_1oHYSQ6)S?<*Lrxpo; z*LBD{-sraCcFLTpHbvF_>e-C?kJ)=CWu?p$$aV~qQGQZU+^lK4(`V!D*p1U3yD~B` zoRW3CvHM!hl@_c2+v+V0URd(9 zPvK2$HjmTE`6^RVZgPqA$%clg&JHo+JZUY=^CBRT@mD}Ahm-33YnOSu=6>%pR|AFR z2Cb$I<)XqZ+Id==lr_tCoZ<>Qu}-^sy4=~d7OFfSPOe+E`(Q_MiwonclTTO8{8U#p z=jwXRTamo$zJb=Pn8mRquGh|+bm6zs@A{~y3tVBl4z9?VxoPIq&zXKYI@OQToy$xQ z^SZ>?D%6QgiSasn%+&h1?F}Py4MSTF28M<*jze+V%O5a+SFVl2G*H_tVwaPPX7iiIqUcJXb zefy7#j0_AK$}EZNU+Zv$?_XUj-1R>vvb1-?;&$24=^|HWt4+FkC?<(rzrDBXb;qU8 zVxlE7Hw{y@_5X!_)^yF1ObiSO$!-GQb|s0;�ci>6UV9@evpHD#sN& zlGyb*rsm11d38ry2wdTJ%J++y7RGH~el$lJ+c=5PtQO4CwF0U&J3=9lm?gG>LzFPm7tfNyL zyL3Uwo5p3JRV2nNiJpFKDSQ2l z`oU&Nvm|C}rOhii+rLn!^j-zTHLe2FbY=;>ZUCt`5T(H39r@8W;*H!5kF_h`t#D*u zU?^!(&?#JP8W(ucU23MBuBQh}MO~+gI)3xGx9s(v<|IGz;VY#p_%Ic z>PlnHd16DaB+W~*-j*7pN%czw^<#kTAAqs~XAYYt8R^Yie%e?A~9%s83~7C%azo2Rj9 z&e^A%cdnUfo_k!|>0*g)dzaX=zfTU?2c6bg^h0asy=AZEy;iO2s+j3vA!EQ(xV)lD zZ~p7M=RuoK5}Xw{%-I$_D!F?hFZbGhUkP$XP6A8feKoFg7kCeDS>s|f^ZfJN+kv-YY?hUV zZMY#mCF;w5*|l)v#rF}fam=Cd#`Ff3>nY^j~JuJU;2vP&;3 zj5M9j&KA{SdB|2}ecirii$SQy$)Z$O9T~2h+vc84HNRjDT7~t3ktI=H`Sd0aN8hVb zQqsmZ=UlTgzM*`YZJkBa6*1-&>sATn&pZ3{NUiOMYaq|;l6AZ>z2g-(YxAS?%br!J z%@(zN_B_GA?_hMtq5_VUO4_Acw?6uNd1i?}c)hGcxgy8!NezMqJd1Z)&huF=vh-=v zB3t{OLzlj-OTGOtVZ)|Phc>Oe%^C+B6mDFR@lU@rYt_%^yx+bph?->}!*w*t@b%i(7@({3B@s25|$N+&diI8NUHteyW(|KZfU9A{Ovibx4VObFM=g; zrFKl1iNZd;=xsh>RS&g{vQ?Tcl-}v;iZBp3k+yl=k%fyvXG`CA5SX^>kdkm!aB;BS z{P57N;p>zxo7SJL+IVsOx^qUpVr$qL7#MzWHEmd;*(Jo)Hfd#u(acLGrM1u4`0tpl z(b~4AqPXvN58t=doh$PGy{>$=y?gV$D4}h?UcUMBXj!SvgUj*Vhc0}5wQ5y~nr_~@ z&laGCMPDSEHY{OnK3H?F=eV#EYiMb#pRA0%NyV#u2R%;M798KOZuca;(_JgPB8Bvy z`Mx>1$^78+9m~GG?76e(=+2XyKb7y>s+aB2aeagA?p>RV1Pp>Q!_+|D{>9L=p(Jya zT2sNvOR0UYvtI3*V^wi_{4gNdduE>q<*!<{aC!V;2tt14F~Pe6FbfI*H7-y4hfx@uSciu<@q9fdH3OovEe~EH%(pZGpVyfgPWxUp(8U z<+W7rb=Mltdo2sJx<3?$=11?D^5gvNg1xgJ>HfJL$6_@p+d6Ndc&c|%ao^O!S<_}d zzdO0^@#>t3%mV8(ZkD;4i1Dx)-8{3a4zwDP!M0IB$92(trh^GqQ@zrSR;?2Ie?+@J zqV&_gSwdIuPB52z5gQ!xeVzPc-LOE5xR9Wjl^3S3z0NrG?_)RF@UoQK+)mfG9`*Iy zbYy?ulnwi5q}Bbubxeu)ia<;^=(a zADe5PmcNshF*`KDdDUX?by}LM7OmQ(J&I)%qa=a;tzu4mv5)OdBlH1_Mx=dYIg zo|Y^6pcb7oYx3JeKU=qzv#b}{k~haUKY(>wW{dnp2s&{|Ih^$ z8XF!8wk*{U;ptrOJzf9$m89!(HW|M!tli%u zJX+-OyWJsw-o0V$O06!gzPEAlwiInYrT-V7-_(}(t+iUd;`I@WK4m*66`5xypibR_ zDS|Dk?w7YDfXb=!&x>8<^mqNucze=2YwCqq7Zrb#pf0^dGEoXNe2)*80^JwJ{ZGHW__48*f=3g<%dTwes&x!sUVUwmFIP+g~rQh`%9`F7rXJ3#=zSjOwKQ7|Gx$Y!; z<2UOpu3x_J_0}z^*vrrKW474c4+2#;x=kCNyqL8vE6PT0d+zQNk6rWa7e9Mo%WJyG zV-ee4VfI+}V%6}S55FB=3Yp|<& z26A6+T(_w6|CH7Fai6}iuUs+F`u&}U^L-}%|2)67`i<1j$F0pNCqJ&&{q>K%Gf-Lk z*Z0qT;-da#m)d4?-7386%ckN!Q8c(O0Oc3 zA8O9-eG+CV7aR&dZ(KhAbjqGTtJki(SG;}h;)@=yZ5bFC4*XK)@b)v1IFtJMWzMn_ zm0RWCNbGw1hD&pqM%$yg78?IT|2|did7U5q=jW9t@AYMNeBH6)X~g{ci8sn-XPtce zacGh- zpSw_}i(dE}&X)W4H7#D=(Xw21B5EO9tZR{JksWaE|_56W7u!^S6rA0ETSd+FvX_Q#`{i}7Pi*^hV)6ZX-}Beb2;8|& znuq1^!wXhx+-BUYng8S9^O?=Zx9{EjYhkyNyzRCsfv8gDS$w}^%erf$zh9YFA6@)D zGVY3J_KcZ1yob8P^8{``yPz8#mw2-0)$xB4GyYDjFTR)`zx{O^`|^E()$eB}{eSc9 z_mi=SL$Hh(I*@JarU;`U9}>3Q$Int$^Dm%eZ*d*7|x5B}ezmuggM&Y9et z>3QC}Puu#^WB+sZ&*v@TOBH=D#=&wV{p8cli=wZ+vYsHlHMyvN{r3)1@=w3YI1rk1)FAcl7d@$09ZROn zuSxoM)0KVUy4O=hMR&&J<*wI$ETw22HvMMN|7VBm)}Nmr^zoKT?n|AmnG+}ZZsR_5 z<;dgNeS5B4W!`B1j;HqHqMWCjlfV1C`RyIf&9c98^Q(*YE9UB}DQ@L-x_QR+|L^k` z!q@NIG-+l*!l&xxUh9HC&i^0&@z>j+*$QiR{}vB>y5P-^y^q-EKX+NB9k)mG%hT?x zq@6()+c#8~O11R%&iDDZY30gDmWO4xb4*JIXI^Oon~ z_j1mAb#=Wt8k^SczPaFPh@z@enUzIyvZQWd@^aRqr|@#A735+r%-jNzU1Y9+bQx} z7f)ItUvtwBly;7vU%GYwik?J?zpt`fubY}4?~Zrcuvwwz_1y3KXYXI|bZY6}AB*|> z)rI`+{|8Of=VoAF*a9j=JS--8s1@&wnV9WYQ$PK}lFzF}VyDT(o?>5H;o{YLHzuLd zs`j$6eEm)fT{#IO4U?3B)`d6L>7;%-_h{+K)^6SKC-ZiHI^z^>_W#rFiI3e{O^)V8 z3dX*P<&e0(+VLd!q2K9q!tc1I9{Jqsus}&l)N#kQ_MTtYBwEUz9&nw$NBdpR-a7ZE zpRaGXGu-=Ckyq(+%=TydHaiE$W!n7m-?_1ROZCTDzt0P=za!Ohwf~>pr~U0w^1bo} zWq%G7A365GyDT86_qAlpVmH3|JN|C_%G*2Nf99@~%#E+hO2k_HZND%2@iF7>+ar(r zPR)L{*xxSt$!Sp6IajbnRb7T_`K6aRX1m24D;%e;(pvY#SLoKs6|*^aOjBAtN#w-8 z@~C&`|NnEEbmfq%oAurkr{t`^{=U0DEA_h08m;%D3!)?T+^g?;9)G;v+5692F7eF&BHQMIGs_mM zz1tA$oP7C-{xY42Ejv#1-;Wg77G3tJCAr^3w;MEZa9|fG<7-F=na(&Vw)@&0#oaCW z_WPqkpVn;7y_vT`>D80clh;I#PBZc5O3QWL`S#t-lQjo6UCmEZc~p4&<1@vnzU|>I zi_20k{5!bsuRG7zb)Ju7E8O%-5*{9KTN=>*bIH?Zs(XV(1An|=EH~co2dn~+Oy6W+% zdoLUBZPT${_v`DIisVB={BBOK(v)2UtnTG3*m7!;$i0gT*P8ciPp!iMgA*47jvwhw@c~Q z{pxEE7mGH7V#fiF`&nM32{qiTzWN*+>wpcSiOXJz2$|5^u zC7r!G`QBHjGV2^a`^&w-tM2Ddb^dLUjfa+NG}dTMiHeCg^%CT-%D%xbk>|3gGn2d2 zbS?jyuKL+OkGgKYH*5E^o8E#gSFc{(mOAsAmEOed#}q`&FAMW!S^bkz4-x$L;p2xV z>8ZER^xQVy`dd?p&pCci#MduVz0~eqjb6jyw6XrlVRz@?xI$6W&0oJ?zAdj8{qvN% zUVq(#xe+(?IEojSW!bz6%D!Nc{BHTi>uQqR`@?37t-YQ8h6ywm(jey`FipnyGLzc^ zOF8{dkLGNjXZGrcaoeuG**~5{=RUb`;e%qH*1Y+ap_A^m{oHww{oSnj_NQ~RraYPY z_W4>f#qteu3q@9&wDsitNGRK3w9aFlUC|zywS_s>AMdG)-sYLudteLytNOBwS6+V2 zmYea|;_vUTr=J;3+p4E7;B}OB;e;E%xBOsC-{}@{QcdM{kh>q zSARw|U-Jq@9;gJ zuHTR@NAp@}sPn-9rHN;rb?sVr#?aa&e@_;7vd7e&1%*|Cd$P92rg(PN|H?WrReIl9 z9;=%Bf9^>7UH){<^hLB$z%=gH7sbuOZB)Xqvb5#z`m#SbNi;KjTa)!W{oP(kr+9q* z+uHbrcH36{eezi@Vw$d6Nb1gY%hV>HU2|JBb;&h_IjT(`jA}2M{6F_AIPYvu=?25! zkN?$M-`ns0B(i3|anSBR9$V&=-`?1}bN*GEnu*(L-p#t71nWnwQrA1*cUyV)PrHAA z8(+Q;v#svk_aXcLvq#YV-TEz5R!u_Wq9G6$@kpT2$R#7H~M7-Zbm+ zM^mrw`d30&n-4j=x%UtcluU`Ejq&hq1v#`}87VS9&t#iD~_s;oy z$~@G>^YYW0w7d5}gPku*SrRpc99L+Z+Vra`)z|UI$JFLF8N0HJJ$okS%5A$mZIa-( z&wgQ|+81X;?@=+T+LE32X#Kv0?Ry>xx2_afr}$XjNNcz5a?!5!W#@X|e6{uDeKt*J zx`?qw!IB%+`Dr|NC!D$Rr$0@7R2N*)9XIDTC)|PYM=XN%@aPQW5@lo=AL0rTvtwYDc-!fOn-S+x7dkQ z_MRHz|6;!iE$hoUzG{*6`vof;J&oqvv5Dk7?Y8#+1NZsoO3wW0iw?EyJ!$^$<5AB4 zE8bUyEK1YX*77mVU;FywB2b$MRH`m&I=CP+YGuf(*LU6cI%IY?mz-<8{eqdzyUHVo zY15htZO;@jk4akXY*pS|w<53m9iLwr%&q-zPQpW%L!o_7N_0c#8<<(t%vjCl>bxs$ z^Xz`@hmMohRPMI2Jn40E+RtD2|5yJ!{Bb4swFxWR)`f?Js06>Rc%cTGWtitEFipeR zJSDPET_|==^YYb~oXvfIe0trcn?3v6pNsz{2~L~3V9AR(fo0FVHYv8Zd-=s}GFmq| zv+9qp?XpVu1-tToKUGt?*t2Ql-yH`wBu>hzaLiD>6SG)IQYY@nZ10jwL3%Ui|315P zyUwlj{R{W+_%SF zcb*YE&Gt*!j{VPM8E;LjnJj%HZS#j2|L(CY7AkOMv3fLBsr9AfZQYQiZ=znlJS!e= z{rX_Pm2LZlsY^~p_*~v1w|gIGlX63;o4_I{t)qU%&2>qq6E?)&}LyxXoO^Iz=$ z%pW;*%a`+sl}$^Y*pYE4q>A_YA&~6>TpWicJ@Xez+jO&}>a2Li&9WAwl8U3U;e3~} z%sx(=JzeH{q2JH1)93s2>90M{yJEt&n@99?93nEjw=ey%x}%QcEZQ>UiR{He!Z04fIptT+w@sh{4IDABdX%+!49o7AWy3d#>R za9q#Pi{8}C0vgHUbG*T>u}O%Bt@!63sp~7>>ncTRCv7aTdb}exwg@x{<>0Bv@tZC5 zYEz?w(bRKKH|@2Z_RVEYo3rBWZN;Do7LY~Y36`D)!3!q2z1~;98#rH2-ZxC>r^(8o5lmsuEd`{yu1s&wmpd{E*+o^MU*D~>S zcPG96`kC{utD4U3Q%@~|zy?g_IApi1;Nr!GWvf2A9D1{tRW2k{)Kr5_`O=lr$WqYS zf)^kqp4Uqb8@TkO8rkmU+OtUGVSz=0#J1dY@Aq8hpq$y|F7PdM)-u+fh83GatnMZU zx2{^{c1Y0o`lp|JQa3VyhPEzj7i_7WCBq>xYt_tYGyRt8Ry(g{)BsJ69X2?$a6)FJ z>W(iUKR1**2z<+3_e84dz=hJ&n>O8C^CPSLyWo;VY|L$fT&*Imf~GUlH-aXn6OtVT z@_1ZUNxC_NiX1OAn(3oA|NS&q%_G??&5aE&e0^D#=iYwQz4afsE49EyphcQLa++!A z)l%o2CwrGN?rqyFyZguLb0=7p^iAJ0Tx>{u@o49x&>5nj2sHCzd3fxYwpC=Hsjti2 z8{dj9bUWSNw#P!`PSdry(Oeg9$9|o!l-I@XbGd}u|LwGsZlGXL?NGRLqVtsS%;g$! zFFZGW?^h5Fy6}3BXyJ<;eNVjNY{X7oES0SXhnTcs(*_=qN&bN@VqC2%Q&J8aaD^`r ze5=0GkMZ?s&=5gtn}Sa8#tfawXFJs1yln&-|H70d5qgG+KnwJA5(j~4&|^y+BbI?z z&oIzi(L*=CX`-$ZQ&0Xrtlk|xNyY4rSJ%NX?pWdKmEMuW6DZ_^3|gAqJNy%J;U2&Wm0nY0`KAv^H+GD ze(iK|QrY?%8BnlEi#YC>_#y45NoXR2;b2+)P zOG#T-Q=?R}F*d?5QbTmp1wnmb&UO~(>%}|gq!#CbM*b6$9R$85GAb{4U8ObGPc3*^ z?#kB6gV$bnlySe8GG15yH}H1h5p8$nhZ!nLO&3gj|DOQqVyG2&yrJG{#?9)y=#8VSt8M_a)F+uETscfp3OgI_%Ag7t>C^dYyawV#w0! ze!;i1?zc>P+flw|)e4T*rkgp}p1p$1;<^ZYQ=PS}wP|6A)n$?VX-f`$O*XrcxG466 zzC+f{veML43z<{THtB?bLi&p8vYJvnPdD8>_AKrHvP+LOZ=AB8zhOe_)QvMT zocd%zCBrYJrVaT5Z^gMZlJCIl8WAWPFO}RLEZ+CoR<-`Eh@)-;tO9ZdQ5H+g)Z> z->DF}=e0|I1QdJJ4k_>2xNS?3(jqODrH;p(etcT?*_ zBkx6=sV`qYaT3cfr!SQyZ`Ym)|G#hDkHazNZ|@7NFzLRa?GB-Qu5iWt=ZYduD|!3KQg88+0XYS{_|%oF$+{%I7iFR^R3Lsl^MD>9yK~G zO_|en@vYf=xua)Yw&Zv&lv#H|RK4$og4UY-tCh3@-uz3+K*x zxcp0F69OeNh6R-h9J9lXCY}-R+V}qJRyKagtEHwS@N&FZ`o+3 zN_6&pEAtZBwg6-YLq-wDp|m9;g|X%wO->6hT;G~9?Z1+@QJ;@d=7fsrT6-L}Bgyq%T3W9cw`%<{E?>O(#f57L-%cl8UFECaIZ5>ODtFzuU0b#L**Rrz zpWK$xwNz=EG$;czWTbH%N=qp?yzOm_p8S#SZnIs}&L}3|T$;XYxlDqTr?=Hd2?gVh zM;~7(Ty#AlnyR&Ls=+$zt#y}HbP4x)IEgMzHQ#aS$xXq7w`{6U_ck?6Xb<~ZlXNuN zozGl#Y08|fsnM(BM7>Xgy<=j`l4x9%(_Xx<>THylKG&@a3b+3){;@S(-HqS8$87qI z9*K`{n|A$sbkI@xo#WBRKNoIYc<4vPFSk_}f5bToPLHvOJ-M7Wd7<{pWz`qf>xTSK zKYFWOdv>1h$FN^1yEr%BTvI2LbPd!-XK)a97BG|bTKRfMqQti+pFh@IJQi1a^0(0Q zy|-08^VDt!cS$I^C`~Mgz5aRC-(S{XzctKdbi7eg!tzQ%N6cGv z{VC~t)dd%?zEwHAGP_{1P;=1cZHHSHt~gl2e(hP`s#~@#v->QxmabS7Te>y{@J#cS_ z{gNqmcE-m&0nHzNX!ol36@7ZWf6d;%=bUQ3FIZ>dwSG_1lO>+-j;)V1*#GCi;icAj z`X?%uOn7UnSbS2vjW3^fib9BiYN&|ooyD7$xa>3db|^^s`eJ6c@`QBvc6k=1E|2}w zq>hX=oqZJ)L*|AB(>|K3mzV->ja$aoV{@JAJ1o-*w}>KDXvT zo$_~ycQ?xZO5Xn-Uz2)n_4@Pc*I&4N@0Qiu7b|}zecY(N{B`}Tx4++kqSfGr0>|vW z+}nW~r=}L!%-r0lU{j&9``w*0A8chOCG-SKy|?w~lw7Y=e7yGV{#^?l%w}z|EMob4 z%-rg`)cjRvHB_5c%>P&NRJlKF5%1xOU6yKZ=I;Bo$xrlTgM z{qMt+izk)~IBnaSmVH~u%w)xnuzM}ZyGqx3KHT@G|4DGY>8@!Tyy~8_FATGKbok&ZoKx$hk( zYx(W3e|k%O+4P)Xxx$F7(&@9Tm-}e`K6cc(&sr^(|B$=<`S@>tO@E!drueK*_{1)~ z;&XQv)&1qymp87I5uUg}FxY9;grIn~ryu%$i`D;J=AsLj*`2u6BTetlx zpL70ndiYa3Zq4)gnX{_EonIoUm3ue#{h=3n`xlNGl31j~!H+D1PO5t#tmG4}QTjP41;1GU!n3bX0c!`{McO|9^Yxk1Tw)U0>Gz z`_xNM?PDgczu>*kuH;qX%3mhuZWjMJU3cN?-gQ;q)UE7G(moxHe03{(x^A4J{;!x< zM;2R!Xn59sz1XqB;bd09@%!TO6~)PpD?;|1S&{tP@xt}!yNj;bHhpZb6z*Fo^UH2= zcIW3y_7#b}#@_>rzwX)2>2&OHqVSi4fyX0`^49Lz`bG8mR&}Y&L-!Qk{5ZSQWW|qa zdF7zoGuk)S+a>DHTq(SzF5>5f@268%Z_L|h{5&!LzVMg#rySo0mK~Z~x_)cLvaK0y zdw;ar)gCx2X`J=sVlaPo?Y~1e*Y69{3xDO$Cx2@t_lDKh-*4<+&hB_Yy5;=Y8CNHH zF2Ah#SO4bZ)9YWq<6}wz&2lq*;ceRRa!HAR7}u`FQu2y>ei=({z8kn~&0DR+qn~{& z7pqm4CT_efJX!75+g!H9ly`Ho_9>|QdZk@jW5=|NW3I9KtEh0>XEQ{UPw~Gz^|a_? z(*GHnx|f1=M3mx+LWD$DP6&v<`|Z1G{Em$aH5Yqn9^WQ>GUdVV+Su@AyWVrQ2Je&U zo3Z8NJbxX`j(WFyk-vOF@uW_d z>}+qlc%5*qy`r{0><}2$zoPd-_(Pu=ByQ^1m2 zhXsZAdcN22wZ0y)S>fZIwa0d;ep@7Ivp;LH!k6PdFMl2Rdg<(q&-04nYZ`t{ebxB> zle&zv^ZB`j>EGtbfA9OWckMj|6{AgJdmrrkwBXS)^N+Fze>it`SgrfN#Q1yQra*yL z>3=o`m#m-G|7%9kq|TT1#c|b#q708m?B3unzoBquN!9<_?>CF@+?c7q_tPcq;uCGv z9KkD7!pqNV7ss7_%B?%|%foh`!r1h8+3W4CldtjZ0v#sEa6=aqCRS75oqYOBq-Fn~ zm}#l9N;0CBm&`*`j+qNbH63%5m=MysgID*fCX=6SxQNfB;?${=IyB{kj1$T_XUyX| zeEVJ8zoYw2N7F`gLjAy(DLWsP6P_e_uK+uAW(EbgD0+ z`r%ro%b9w$nJ>T#d#r;fnI-!A+& zyiD_o~ZTlRZ;?Rwu{laT= zC+=I@V&Jp%jgjX!0qq5cc7J`(z3+?p@`5D>pk&leg7if{k^XkwB`PX%I8zry^qfm?y)PFJULzNN9)eCxO{=Mcfadrvaf&l zH}m}O{lRv>l7AhPi@({ewYAsr#yv6hJD)^Xr|tVuG(W8VX7%~PV18>Cub)>opVyDr zQStX?mSe=%J;&$GiCei&{=bZI){?qQpa0Iz-@p3fXV9nzg968FzfJp2t$1&~gY)4p z&BZT8ID?EfgjzbT;uMX%(%X}BG(-P%$vZhKpzb+un}acMN~p^OtZLgVW_>~9vG@4x*2SLl20 zy?guPzQ2_I(!9;S_GInLe?Qjk|Lb?_$rIWAme*(g&d3+o_Wxn=yYBDb|7|IGbG$gc z&f8w%TeEO)Y4xfgNUdL1p6 zEPgvdbMo~yvEN^1fBv#RM|u8U)yZ}bcO(a$d?I{#e&nv4qsNN#?w83sf#whzxfj)(xD-0;qx7!! z4M{6jc^0jl+U=RRR?6Ju@Q#2q*PX6;VIEtnUjOypSF-u`iO`qb{;I*tB;PylXl@Fa zd9Lfd?Q)&#b#FTr_uI|B@_(1a`TeiEk50Dp{_FLdSNuwN>&-XOIUI+cOs+q5ukz-v zw=9~`)&0U>au%sVwR(bQ zyx&%SeZy|SU0?T1Y4SL~>+!NX%IBXP@6Y*p=VY>|aLe)TQyP*#vcu{>r2jjUx@Y3* zOO7*gGcU%M-uYIU{A-q**wmN*KJaU|*H3VBye)6{{M^6kb;smwcRt_0uA{BTbo$;! z%@eR%eN?&yJi}3_x+N0{jn=~PS-x1_szbxagQm7y3F0r2j6#Z zn|N~hzgtzew|mN)U%j#EqO!Y8-HU_k9^3rA)AN&Ce1GXSUC43H+mu-nFB|e+Ht0#( zC}CRm`tJO1Qik`+EIYmC{wp|N=sepqHet(?_VrI!mHTR@1U`hV5MZS6a2w%NYd6bsqAw~xoo|Jl-YPlD?`?f*zi{fw^azL%>$rRV&H z7pzBn6W&ZI_~G(us&(DpkH7jp&t_R@|Lwt_>iv>EmL1Q-tk&Dim{B-=?~9-RYLEH8 zsQ)p!Z~nE5_m;`q+kZa#Yty3AU+)XA&NBTai4?7eEz4!M2SjI-tbToJj&aS2@;)0A z!@QD@6OZriPQ9L#c{}LigcuvG?FP^5FV0{8f2!x(-`~tWe%yR)x8B2)zr0;jv^?kR zEdCpv7w@=oWtR2Me%Y6GA40yj9KPkM@_6m*uiDIkQJYep&3Wa%&foNh<`-2SNuwoE zYro{XN^U9?xc5!A`047e>ep-kM7=*yp1a4e)}Uzr9m(lzkNINjCoh>~vwhXqY57*g z^0$9QzlrB|%3C7$bM5x~A1_Y$`1N^jsdv=7J!j_r4fuOIRNP*qw5K%f|LVuv=Xcyc zShC0S{fYTSfmbBu=l^?ds{1{Ee(y{7{yUF%B=^GFoH>XV+j|6AJK@#@3kk52;b z7CNo^RZud+|C$Ibe>sa=RMMaO|&bdDCm)qPPpg3{jlwUf6fwrRg zKTql3%eZHMJL5yJL|fqP;zhUiOLLe`l$Gn5dB`jF*{7Rlw6D#-a{k5h$d$L}+1DO^ zz98_nM*SPbO{ps$shwYM5`K2`v@r4pTE0*4>OU@>YNG zE1D~N^%V{4lU%Fcw!U^}nwse9e601b+6(q;m!_WyUH|pHah}q{UCvgw?*9vFy9k%9nfa-!spx|Bla=xO7ha-P9kS zSRYT?qtrU#Zrh&ScVjZYtB6G`=Ln7Y+WPuv?dG#=>*MF!P4$SF7W!2xDnuC6uVg6W zWJxr>q?sy}R_^bwe0pO|@tqC+?`*@?zP`6Z_Uq{*a+Cf}&o4h>eM#Fm>iXii+PmCK z{;qGow%25Pz^7clBRf8x`|T(D>{gLd2D{MZSkSDGWREQUMqa5S%3etE5ufH2LUt9+nY)k zigq1k>z_ZV_Q%y_D**)7Y&6c__xpq<4g7LK6A>~TrS++Y)&32*H+0iPl9Y4XV z5g5+$I^Nh5bG&!aiU6(L)Wn(FKR@TbA^3Zz&0K>R@3r&wS3T;LjHubfTYs(lc^B6q z5!H{27KMn_n%xcd@4ryE!b~Re!(#XAQ}rVDrk#Et9q;G1Jf~FpnmBljuwgE<E2QfcU3^{TG$^{4ksxkXndd)5&z&+-RpsPsJ7eI6o6U=cw5aGc}?j_c_^%i+FDq zld7MQcRnKFf6|`~%5$$Qig`D!>wnI|FHr)eoojo3yew9eQJ?BHRXdf3ADqoh>{$|n zjd>0mtu0#mD*toCZGO8)>P`oJj8t6Rx4LJ#OSSCTa3*TM_UJ8`q`=Q&r- zESXgt_SGN~G_%W)kl-L-W_@mFp40iyUQd_ZJ9==_86T_QwPD-5cYqytz^_3;hJojB z!J7*1E`yv)D`)JAGJd&5KBUyCs}F=Ja^p*ANck)enp1rK`_i0U3%$V+a>Iz_p+k~{TW+J|eowPs-l~(gy{)JVHM#E+29eid zdB||t+HZ!wNLiw=k(cl;Hr>;oZNVFu84fVCD#%nwByY>rUU}+ik;L1kor{-c?pl{E z<+e3y>W+MHXEfn}lK@}Hw$#@Kt3y`Ld;WIILZ`cn`f^`Oi8%deTtDTOC1}w;0|SFK zQ&Yi%McYzes`YeV`0ldh@blbbrY|hkJ#GnHv5*byDK;yXhX;;XZr7PD6tptrkHQVd ziN`dy${ydO(_C!-VR5XXKj=Vs28M0cEDzruF!8y(sUyfyplaWC+2*xkbJ7m^zPPf+ zU-)iZI(YJ?A(qc^$Lxfj!ehLL7ff+We8T!~+nHBa)HjwS9SaWP{pFPMq z*R9|b#goQy$dL=QvXFs+VTdpfI%*!XNR{dp%!-`0&QY;-)kLSPmUpjSgM*@BE{Ed| z>7bS?yZZx=?M#%2vA?!AyxT6eVToL0iR~i(CxzNZf1~DhloqObN&c6B1WPc-p}W27 ztmif?OT40d;WP7JZX~J>NmGK!fm*qzOa;(`Su&{2S$nIap z;KX%+uTkNS?~4P;QzA`&6>bdCnyT$6aAa}I*B7gMS&qeL?>F1ibLANKWB08gAzwc_ zAItFeR-5W!bgV^SYF6lTMK^A+DP_hShwe6>GrPQn=bWt|OV`}jJ5i{_=s@g82W ze8H5a{f0U_=DzC@rDL4{y$~g`-x`fYvZSkwV_@~p=&n5j= ziwn}@nuAttJGeJxO6ivEufMMk(YF2!PWTK7)s6yu5!xIz43WfL?XC+NJwxL;N#iFkbC>&Q|_r*M<2NT-FGmyHt_XqgNM6bmPBy~viw`O zPYLX3P+81yK(bs$J^q4e#M-q=hs(Z-?p=7VWm)F7?c3NK1v)dr<3W?&3=Ae*jyK+v z{JXhLxGk~V-`~c6%gn3|epke1daB(GF?w1#@5mz1JU#;hGfUHko0i)a&n}29_pkl_ zGi`?EuFhZEzD^Wywzgg>;3)Ctl{PpC8rl>%X3z3-yk6-2v}k3B8PC1^|fW|A4CwuHCyY4T0cEM0~YPo;z!(|C_t+|*ud+1H>Ef+RNw>pxHqGS(XA1CKv=Q&+O>3@ZUwc*X`gO|1 zhmmF1wj9~1t~ys|shH}ry1hX@*Ar?sMe$|@eP5vDq?#@f)%VTU%onqt6(8Gd{WV|G|M!wfnzEe>1ya8rn)B-C^{KnP7k&Ib)vHtE z)Dq)$uS51~se!L@VKA{|Nu0cB$}5F8uio{szsry>ldU=LcuxOZ@tU5+GV63)Ehk%X zW}EYFs+dt&8?ow^g5uTEh|^}OkJ4vM@iJ0W`Plkp%0%VqcP9QntXz8|Ho!x&?PABn z!%X*&i-Fq-48DAhH8HQ9u>W;TK5MP#b$QCcOJb%|t_WsaS=M>YFR0sb zsYT+}jSj2oJ(td&aAlkD8h#bKP1)dBMrgp?+GS@*?$ z{Z*_FNttg)bQd6Gj5#o`ddTK2v-`*w1xus_PmlR(b zv@CH;+UBQTJNMUvBbs4@u>!~JiMco164ylT?z{G^((KcuhmmD(E!PHrd$^}pxLa6H zGdU!Dp`%8)6<2ERoCyU%^7%dck1cL2{B`x3RPXY>RV%iAS)uSzO{i%@hsOL4uMdB^ z|2?k@kGXk6%J{3=6-SADKZ(}cU&JBySqn97;0(N8SQ57O?JKj2#ogWaiULnR6!w{9 z649mIT;%uop<`?V+eN_@-TjhVquF@FW-G0FB-ax>;g!mgprhM=zs%2$UmCDgEkx%^ z3B=7849FfdrlfqJp0vTpOU{d8LR@#muM z^?Ayi&1x3j26LNY+U5K6rIx6v+Q@wmZtHok^2FzAlfk}LgF4x@{&Jy#Z{?gEOXQ9P zyR}~GPhH|6ay;?Mp9BHE99m5)!jo)Xh$!&)wE!yBFF5$M2Wt)Pd>FO ze2|`_U-kCp%;5e_JzIQp7Wp{w9`aq69WXsnrsY>-ptSCimYlQ$dh-mV0!2?%C`zq- zv~zKjk)w;sW1TfzfjxD#)G~Q_x<~KkJ5;fA*1w>zL-O@AN-kgWyv;q& zH2ggGF(1L357%7DJwN~6`^(>iW-dHd!`*9#o?ET|S5WWwEA8sv`!`FkU2TzeUiVkO`rl>f%x&C->j!pE=igs* zYuo-z8Tdyq%u|pFORrBXZpjpYQ;N~TB1L$xom$o`p4b1+P;Zz?N3(SpP75P&u`+bv(3>l zS|PUIc6|S|`r+H{Yp=ck-FVsh{C@|pW6CW?afMs7JmU6#XV*^CHI@1);&fu$wlGbX z8~007|Jd?h-@5G0lASeWkA7Ou370qDHEoUcZ>#qWlh?MMFOU1X@Za3BhA;J7hRi}YyQvwQS0t)e~rwp%{nW$ z<&ywzoX&To1GABfT+$LoId;hxM^G`@m((-c64^T0L#K{2sNRciw(gFZUGaI&)z~?vjmVjQZbSL-`w=bXT5UYQ?vwBB} z^=GbYpKhis&c5a^ba;{D)X*ZMn^AL1YW{u6KM|6B`mVj#o&xc;Vl(#Fmc`gqd}93V zdn^6xt{@esXGA$e)ij<4@p<9F>`sBq=| z(;I$D4`1;Mz0bTMezn$LW6r0i1xJ?j&d=HVG5chCer2|Rn{0CN`@e15?|1HXmwGL> z{qBdK7O%G1IF~I}ICH1GwN9q*PW@lWu>JR{PA~n|l>Ykux;;PQ7bW)?|9<#QTJOT~ z>fiG=eDu7;A9uOvzG&^QhS`g&4R{V;f2MmuuTChKzjt~(f4`aQx!lu6 zKYY)xUw@{jonLuB9bA@Q2y9Usby#Lgk#>PHypWB z`Khm3+$(L0tyQvH<>3k9zn`cdpI5bMs?U++jPk$DF_l+e|6-i_k+&_`-Ey&;^^~r@ zEf$}zTss#w-PNdT36Cfzr|#<6@8U`>`sQ07+o37jGA%VVC{zDK$@VWV^A#ivR~CJ! zZvNQLn!Id-*SQ$O_Nm1ykJsnx*W^uB2x+@pdT#xm+P-Ch3A&XNe~H%rNj&8l)t$CP z{@>Q$pX|5)aog*7!t1Wl%&pV*t=J*TH``&y!uusBO660h+T66@sQ*9nQQ`AfKX}}H z*U2ZZiJdZ|a{BK_%%FgJysPi|&##i(W^*6%S{k)#O8B~%%a?6RiU+Vi5KU-n%?4jJB>YS!@GWcYw0o$+xNZyx0Yk5#w*7YTA%XE zb@#kb+dgsirO@PAY1@__{k+G+#H6uU`0B~!uc9`K+f3g3clWcdonO_>IS<{Ld0$KR zcj4a!f7`h4M1Gtn@Nb9n?(@5%QraTC)3)CIP`Nn$CwMwFX#bsW&-Hh%w^=fOjgRHt z-}~#=$?q_Jm-YX%_(?a%8y|0fKH0aYy&!ou_o1iS;kF;IT>h$f(0Q56j`w!LV*0WF zZcO%ny8yh1t>Lb8(}tH{N?Sfif8IZR?HSK3+4* zeoDvR?A}~Z{9E9a-Jk1+v*%Y>#cU5dH!pfoOTXO1Kd+8g2MDe=isE+)n!5e&#t+@w zzZh~no~vHwA@D7aBT;wO#mcMf*Z22qy7N+edY|?5P;M)Zx!33SSRFg%QZ{+A^y;|u zx|QY}!sqYZsyeM|!u85N_VZ5Z=8x?gldgBXm+{+X$)S5%>e9t^yY;5)o9t_S@T}>> zMfG`6n{sNOMlJjKkbh2~hS>W(>A!gbL0yFd37|>flP;Sgw0`cp|3IxJzM}loBK}=l zuBLtT33SRmWl^0OxFaF4b!X0>jOzQfFBE+iRj_qqqC*`QoJ~)VXnP$sLYn zPF1CqB_3=pRTp*3_5Ktc{w@%;ZLa6i5`k}}r!ww_H3w|riT=4({p;kH^?z4AohSc& z-v*@C~zw7?acYmKK|M$Z0 zW`jLHzrEe_8{9GA5dtOO0^z4#M?e2z`(AASR^j!o^IImAbw{=oc&)8ylt|M^GC0P$ z(Q;vJ^pnXgA#6^rU2Y*+3qu5#?h{fi{Jpm}GUQmN_*BlO1a{WM8Ap`1 z@BO{qYfZj4zJJN?T>nR}`0Jiroj2`X$!&;!kkklFb3EUYttKvh7n?R3f&1d*A`*Z*Km&qsV|1bS|&AU22V%D$uJNe`Mmh|N7)VySz_%7huGQrc_t21AGXG;Xt zJ@$WI-<$vJ+>RwCZ)KE@%8AQ6DA!9jUohyomg9Z(^Qos^kE4y0IKZ_IXy{$`_NLYe zPqSQ2uLUlbNs0Zvvgcx!u=`QR7Z1a<%adIy z;V-u|Kj(}0715wY))bNHm0rz@1hm!4Li z>s8p??x^s4yW!uk_lhaUPQABLot*pZRCs$_q1DRVyoEb%e%_ZK-uH(?`#WeMLiN3A zVbL#noywot=GOnV;?27K%W&T>@A5WfwU6_j9@d}j2A;?5`|oS}?{{6z7B}#Ls|*QX zFI_gUe10@&<;R)RuFsom^Y@2{=d2?kXUgh6oZi}RdwK7}b|pD&mDk^&Ox`lDRVmJ` zEx?QCFK_pB@vHGEZxdtMwl?aNo&WkfD0I?71;(kiPYcf8+Pgo~w8HZj&)T(XmAVeC zm>>PbbknJfrw!%L?fx*oOcOePXV#2LanZ!mM_WD_%3m*hv9F^3+qu&bTQ`7bY=8ZD zVr;%!|9`My+}xU|rxT1bM7G@dcK6EVkG-v+$;^Kr=6$cXma;RvcjwW<*K0(JEAmqR z+dURT_S9j6oYUvTxq9`_Ri3?h?xp+o4LvJ$WLw^BdVS62XHDhgWWHs}{eJ!8AzinY z3Gd>mm0aS{bnLgk$KAGH2lcu7^S*FZUH`gxWkAUF1^RoJuCDv;|GxcyMuxDHuh&h> zl#b`&i&xm23rsrJ6re5l|Ky>Q^TWfMDqWS7X69V=UnciE_SeSOmFK(G?m60~uw~jA zE;m_^!>jm`Q?0DhC;$KF{jKl)rCTc=t2t^!wTUjD8(ii7XU4CoXYA`I*8MR5`@1K) z0kXt!E`#HZ5{t)uk6-X;glqBYnXJ;?wQBBi-Dd_f6}-bPp06nvto&?qR`}(<`L8X! zj@=d#^6YeP?$Y89T-&9&)FQYiu;RA^-a+ITB5m&%bzn_UdP2{y#N;3wtW7yDj{D%K4;Z`MUXz zQt!(uR-Pz{@{WJC_k6%D&;0$utJ(K>?_d1;P;BSS?_b^<=N)upe>dNA`7-;@H3G++ z*I2Qtzumn3wZ-1^S7dF!8Ymw5lAfOP9dvpHL&HH(aDWynPjPzsY3H%#50i|ApUi)jT_dBm`|B{rPA$h z(EGBcTD#7~>&*soS~)CNWzVjB@qA7Ms8InLalg?Mb3CV5(~Qb?$Y@2FF$PE+emwt@;SB-FP*8b=Kt0)U47@md@iR@Zl`V6Sw26Hmpdgg zx$eZS(@#sY`F_T4e`3C;(AB1U^41iiL8m05rp!6plm7Tqk$B zYz*OQveTbfbS>~;xc!BRCp9Mdc}>>#tN&JiTK%W+|KzvUD{uXWaEAwlrCwUF*N z=Jy8E%JSZM#OBxE{9Uy0#@EYlZ=U`=r>S&W>edvaU2MO%$bfdaGcddXwHVT(x4qj^ zv{PqW;?8q>+cxdBs#$tsW~cLgdAa>NKOT!;H}m7#sr_Gn$9nA1l|Q}Y(#K~XuAbU9 z2R#1^Neqiurd;B0O4_(1)^B^&C-M7c9HnLR_Wzi-yJ&s>>65v~J=G^44Vq}SrxrRo z=frU+Eq&&+%vno=PQL&B$|%3oqv*%-cA1)pq#qo{q4jBf_qU!nZ~sJpcjD2WTub9* zliQPe7a81W<6H+x7{-bmvt^fF*`(uEsW<<2-03YctC}yo;yv`vwN7u!?JdoF_MJ#k zT6p;CXWKQZ;AL?J0SX-K40EIV-nuwV@mi{OMz^+d-QykWqF07ZeRX?XC&XB8MGkhh zWs6VyR_t3pd&A9l7rd7SZA<;>DBwCn2Ru$}FhPNXod+~`@Zx^hSL?piSuabXyk1t$ zQ}Q*h;shHK-Jl@jQ6d~QEmUjz<6l`#I~Vums!u*?z|*{;QwlOuC)J`Lm;8 zc^^+d{j_ec#N1^sL$rh@zW=@Zn-|!a1@!_g%>2*WK#iAhtx(PKgGzdHif?a=JQ|R^8KA)?ZEMWXjP2=cf(QP6F@p9xYnjBi+_# z_1EIuK?|M?v|w~QGTz@Vv3iL>!XS_Qx^wqs)J0Zy*Vb(((GDmFyY$b2?E!Bk3_q7bR034 zTGiXm3${ODf`b5G(94iruR<4V+`1|b@x^YQrh*BOCM*NPU}kPu&~k0!E&c1I%?ceZ zE+Kzb=Y_{BL&7M8W>P;mCo@=!G!>X1v^3`puIF96B1Dh9PQY`ac7I+^jw;7%VUO37 zJ$Rd?=crHiJie_|fGtr&brZPSO4#0{AoD@u@wC*J7e3ppUsY+>y*A(Bnsh_-9?cDP z@(+I<|N5KraWvQ0@RgelCj0H~6W;G?^H>BjXSiXug8*O7wA9&cf-H{f0#>|NJZS#p zL6}fMw%IACzpj#P53Ta7ruMiz`Tei#HzbrcyjI{~H;BHzAak1b)S#6zT^-k*3Y>ZK z9xRa56_FB8@VU0V2`H-U5n!Tyu!NWP$+k(IDsad$dx%Z{anqKaAFTUU15V-J4 z{`7*oC04EAV}ThYE(o+RbE;lhqZzKXv!<~)Y=3e=dR+6GUg1|pB{{p-Zr!>qSusx& zJO?c?L7;`%vo>|Q*V0+qPdsz~&X6_tE#Xe_adq0VZHtPK-ZVLIL`p0WXkm7<3BFQT zd;ayw=dbT?nmey)@1x(G*^xIU`Mp*RC^mHoLUA7$Q=M|s^rr;QOS z!^+>*HqQ>e-+joS!aaKT`)Wb48Q>f(p&-z5R&S;Uo8yJar;mGg9sWI*Y^Cs&*m?k`B`gq)sfHLzl@KlXPXOeON%bu zlJp3?yYE0lgF;T+oWkRUyLMF{GU!q7{#IM^;m+f?<+77zZQ?H%>Y33~s5NsH?|n_7 z`h`{xOC*kIA9QhX4XV1h%CzWO%-5Cozkm6<_f>Pt*SoK@r*=)4VZj9o-3xC7TF#y} z(mi%#R_W`W9c+>FXsf8&(#>A4&pj z@$+4kZQ$ql?HIG`ibYy-0&Dzb*Y=7Y+b(#jqG!EX|C9+1t5%7qmY$w`Z!3R@*t*P; z<3DdHEPRoXe?3*Q#j)Pb5)?KJZ*({gJ#&dY{?TTAgw{))oIk1YmHFR3FPAh)-M|t& z$7tCS!SzbPze9KDPsp92e(SfNR$Q_hm#gYTt$#~f{>?kKU4*Sk}w}EtJC}Jd*&5p zMced;2rQi)pdq9cG_89&tN5Q)7R8A-PE6IfJ1Hn_bL3x(BWoZzU9C+a$7)K@^}@pj zZ%#fh6*H^&{b0M@6sz|k)75z&8Jy}gc_gP2@J<0GB^P;Z*4`BwZ2SLJzueAWrClu*mEgADYNE%E zb=_Otz*%sXWYdP1FF39^M(9k_ikoU~6JPT3t8-?TX1k#BJk3>)8XcGJ`1SFB^yf=^ z5@M&nc(dW#&Dr(8CndH>Xw|h&YT5VNc75=ooJD$n=3gwoGqE~1?&FM=GxKW_B|bm4 z=FC>^REpDBrsb;m=S^vA^}Ns0J0<>d9X8mct2^(x=hYWVOJ^_MvG9$`YO$=>i{3E> zo;J8>cPheo{`6htC&5{R;f(@l*}K%T#2G)G96MGVysK9C^X8k~{iQuB%5#FwXDJe~YU=+!xqU6?^hS5N zK#i&MDqhXh$SV074$4vu+glZox3((mdAC`1xop1C^kdc`FVs@Jj(Sht^ge#)vybVg z)WqHWawB)P?fF;r@y6oG(`!m4Lbon;)!g~$)Kc|%23i0A`nVmcI2d?7DAij;Cv@YK zE6=+;7CF6Hb;&oP*F#n2>&JC&>)bV!mb`fL;zP5;i;S0t*GcAgkiEzC+)>f#X9L2T z%Jrq|eir_coSDV{?MAqLvhLiY#j8P6iPH-1o@9SxUey1vFZcTVkG21ghCkc$>sL*v zv_dXqMzLn&j~kOO3xbZaVA#+Lnor#JRzu{eSLD@cukXD2*#0In_WRE>A1& z_a^;h5j?U%X`8G`G1q1zMr3!A+}mz(^}K$+tW5IbUeEKlbX!swb{O0 z-=Eg2%>T9e;?|bXPuBJvTQ3R9_qDyv)vYl5c!~Kd_v@bzo+?@Id3$WV+PwYAxAl{g zkINQ6IU#uX>;2nrcCN3SXI^#XM542G>5rFxqW2l~`6Dg;pX{OHwRBToBb)ggi?;>e zZ|QH_AhP)5wB}#)ryRHDpH#8sL!W(U(Dp*J86XEX8*d?eeYYEjam_BXi19R z$KSV~+x~cU%0A`;->j7qD~&v#J~BQHokhjx4uXF@H)94@80$Q zITB^0U3}huO{)Io1zxFJb}CHqb5o0Yv|C-fGVJW+V|g((#R7J^L2pkLWj^_Q=vMFl zt|?OkdV&QnT?k(G*tg34{O|qRmt}9>-Z!`6pWojl{{rvZJxD9rvVHESbHblm4}I3( z6LH@DvdMQD^Jf<{CyVRHetYsI{pf6gui8uNf3$B(ow2hb?b*R>F7?`Tx4*X({9rlc zTm1F4&Gu7s3)4UCy#4zNqj~z97lH0FWgq5*$Jf{1Yg+aE>U^17mMOOagNs|6^c?=i zyXFg=oA>+Gj*ZEa^6os}S(ErK`?J^6Ph~qn7f3Kj*a)=vs!#QK`nl4cLrO-m=6uNW zTkE$J&DK7aawJpL)h3*)IpE-(hE&^mD~`+-FcWC~UuW~{;MDbOVUJdL&W-h>n?{}(REHhO8^-WGgv3E!c8>$HRKes4LX8Gp@m2ht|hL!S<& zKe7AcJhxR^eoozs=`(+?Tacyw{w?E7O+V!=i9f<{B2V_|GDI+^WX1O*MFZhWqNNar-8(qyI&95ee+IZPAt1{ z^=;Wxzt^w$CGvV&YGT*@n#F$?PXEN)vPhxAMlLj1-&A1Mx+L3x#eTb0>ppwgRon{v zZgu~*)>fHEO0mvN3(e}v?*DzQb5wcmoz*pe#I8+RIdgkvuvlVjC|~Zk+-kn%?YB7$ z#W^OXcIe7$_F8MGuQ1)dWc72U%Nmj2V}IW8D~VG`(Y@7jQ)j)J*gfym<;nN8i$3RV zT-^EIve=V#Z|`&_J)*f?R;Cc%#vR-E?o{F`;^OGe`< zj!&6$7G3h;IisU|=ITVlS+l0<=5Jp%aYuUn4JRA7w29MeKKp%_HD}XsEsJ_`=FAs= zFXP67%ge60Un~B3NA!1_0>`(omRVN2ZmF+~EZ?w2ZLZye>d%@BA4hB}^iX&cw>OmE z=kX%jrP=Qu8ME5^hI>{V^}jl6_RfInkH6TDZfkp}XDGpObo1ALy!@XRmi>1B$XX>= zclS$=;4P_RcJ<;`_5%NYS-(+!ZS$u$-S_>CMgJ@B{CxO&amAKDcegzFb?Ow?x8ob0 ze$P)YpLc@IsqgW(H~%+mUTs}{>&&gp%~|PRzI^+eczk#G*E&#z&2Zp356hvZ$0Zhv zKUPFt{&c4}=_k|E@P0AD##Y6N%T$dYxpB5H^*Y|P@cDMRH$BTOSDl|9Zd6~k)7d1_ zk6V4sFJ)Qw*THH}jdoua&N;8X^~UK_`BQ3BBRdae-&~|~<O-L4+? zG(6;(oT7+p|LwI|vt}MI+IL>cP+-=&N9UHmIm;Zo`}*eMqIjP(zi)rIZ+raRz5V}w zdp+)Z@pko=SC!6I-}nAh-nds$oMZ3o`DO2;rd!|o|5W{v_rBBjSn2}2FReVy(<1sb zm3#gC>vt~~?^tLk)cf@34;{bra}0B<&Q5VL`g!o*u66Gcw%JN^L_S`}|83=}X}N1w zt=UoekT=Te@2%OD=l|me>30Obhu@HknzpW=J6$WOs+S+oxEYMi>|TDjGu04e;1~fZ~XW) zxPPD4^WyL|?2(h^=q+Cs`$_818bRiTc4a}|ALnmArda5=dfxrddwbvX{(tH|_3-UC zq4mbo_+IXLvOnz9@v^$0kZ0GwYrFrSVl5S4;Q#-6-I;qme_u{-S@-NTPfPInnI`q; z-W1d?*|%`x-x&9K{r1VXu4FI2enhylZSnD2x2%g=EA@7~b$Pmjfx9I$w%q|dpvR!c z35xRona&ii%U5j9?@w;r^zEWjpL*F@iSFf^H!rzlam?7fVaB`AV0Y`s`yrpJkYN5#(aiR|0<@XRXT%23Iz%nNm_ zWw^LorbnfkWFK+J5D71}%{)KhOV0oD^Va*%)CR7Nyi>h@F8^O?xj?>r|2rF;|)PCPAncdxu*>A}y>?>K>H$ru>I1y~L>DSL0qdvP*F>-^i_)AS40 z#=bS?4SxHGm+|MSxh5-a!gGtB6o;<~pMF`Ds*P z%hMmEEP2#^ePQ={#V@(P7G}Rcy|`jaqJWcHNa!`c`C8tW4j$%R?zf%Cl935Acd?^M zK|o9KT)0Yymj$OL2H(NtrOFmco-UoqSISO-%l7GCmH~jl}PPMt% z-jC;Zep(yua&^anKRd5$ZrZc|-G!CL>GsyQ#ZWvtPu#_^<5*Iq*z-Gkc9k78-y34? z{QLG3&Cqk5Ps8GuU%csO_||%L=$$g>Uw0(6uk~x3T`Qa7d4AFS9J@yL_>H#@v#Wi1 zXZuvwZ$tl_^(`!`CT4!!Q@$a^&oWSE?$^Kf>mF6aK0k3eQrFA7yL;;vvt2@qHTFB4 zkW;ETe?BNa!Y^_~cgdrYQxe9VTQ`4r_V04kOP{+O(@(G-@|gZoS9xQci^b{4BYd9A z`KIitkT-j?O4pfbq1`g8ZEt>`e7*0~y~z>%&)1r++biw-Wx=srlb=d-9SS z6Q1iHo^qsb&V$w+M*HT>{dTMP>*~07fuB1oB&PrPt7&SwtIJ43oWp;k!w-F@=O1&D z?{AOuIQwT(#jdZqV&(IOzm5gOX9{z#eaP{E$!>FyZe+*|QfKXg_0ukTm1Eqb(8FFk48`!8V8#)GwQQpW%AyQEn%S0wuN=Upx3I zZf@QF^gAi-`B|oqQja9{v~GA3&@6khzyH_tJ3e149{s&5Gk@*QCSz_+fq=Sk{lCfZ7g+;Kf|LFw$;Tz4xiabE%Fm8;gjk^Q$WqFt}u zS>))HS>b`c;8p~~fq1Yp7EL+8y|AF-NRiI&_mh{|lyrBV+`ao&ZR$G9&#O2KZ?m6a zF0N;^zIoPvzwo$d^k{Y@t~rU7?0`Pis`zFE{motuVNSc(@`8HA)Iyf zs_gssJ!fVcUwgg8_V(;${}<#QasNxx^c{lZqqZcIEMvT|ZiPIZ6zgwI}Gby<_E zRtaw`>oVEbRm7#HnZc}W^1LGZ?bKp&)T1B+d|K8m&=e$6kMplA-COnrdQ9Z zJ>X$m28IXcS(p+VC%JS6N%bCWvur>7;$Zc$x~I@t?XoVT6KJdnA$mpLrkZe zPxvU=X0|nD&s+QYS$Ey$ozdp)-Bn$CRre}Sl&M?^sS6k$=yNb7HY&|Hu}P-Wt$pRC zlT3-9+cY;mQnHm>t#Lf&WyqPG2I{vn^(&{bIxU&9*`$L&PpBj-SPN=LF0u@YaS`>Pn>CW z2C|8kVZjDR1)Z3#H4<%ty~o=%bw0N?6u+)NC$_p$$5ODnsb#s&xkSO&iPbOs3lfcc zeXd^Wy?6gz;IV?1{R!zNvdccr$)0lZX-7xLWDQ^C!v`kuD_)jM-lDv;E8-aUvHx$= zb+ha0-z9yYtgC)_QO+x)SCB~n28Mq&3Ibw!^B${ua<%@}l+}w|yKm`>d)9Lsml_Kk zPd;?Jlkc-gTO>X*3tO%9oY;5n==ptL_&yb=Uf;nHrsKq>)g9CldBr4XmXxon z$=YV0n2j@LM`Y!@Ho8vPt8{JSNuz}`e=T?^KmYxWdnR}EKHq%ks3z90% Date: Wed, 22 Apr 2026 19:32:15 +0200 Subject: [PATCH 08/18] many updates, such wow --- community-lexicon-templates/defs.json | 16 + .../invite/create.json | 45 + community-lexicon-templates/invite/list.json | 35 + .../invite/redeem.json | 37 + .../invite/revoke.json | 34 + docs/community-spec-mapping.md | 5 +- examples/sveltekit-group-chat/.env.example | 16 + examples/sveltekit-group-chat/README.md | 126 +- .../statusphere/app/permissionSet.json | 26 - .../tools/atmo/chat/channel/getRecord.json | 184 + .../tools/atmo/chat/channel/listRecords.json | 256 ++ .../tools/atmo/chat/community/adopt.json | 56 + .../tools/atmo/chat/community/defs.json | 185 + .../tools/atmo/chat/community/delete.json | 47 + .../atmo/chat/community/deleteRecord.json | 65 + .../tools/atmo/chat/community/getHealth.json | 49 + .../atmo/chat/community/invite/create.json | 93 + .../atmo/chat/community/invite/list.json | 52 + .../atmo/chat/community/invite/redeem.json | 60 + .../atmo/chat/community/invite/revoke.json | 46 + .../tools/atmo/chat/community/list.json | 38 + .../tools/atmo/chat/community/mint.json | 51 + .../tools/atmo/chat/community/putRecord.json | 73 + .../tools/atmo/chat/community/reauth.json | 60 + .../atmo/chat/community/space/create.json | 58 + .../atmo/chat/community/space/delete.json | 47 + .../chat/community/space/deleteRecord.json | 59 + .../atmo/chat/community/space/grant.json | 60 + .../chat/community/space/listMembers.json | 58 + .../atmo/chat/community/space/putRecord.json | 71 + .../atmo/chat/community/space/resync.json | 47 + .../atmo/chat/community/space/revoke.json | 55 + .../chat/community/space/setAccessLevel.json | 60 + .../tools/atmo/chat/community/whoami.json | 42 + .../app => tools/atmo/chat}/getCursor.json | 2 +- .../app => tools/atmo/chat}/getOverview.json | 2 +- .../app => tools/atmo/chat}/getProfile.json | 2 +- .../tools/atmo/chat/message/getRecord.json | 184 + .../atmo/chat/message}/listRecords.json | 38 +- .../atmo/chat}/notifyOfUpdate.json | 2 +- .../tools/atmo/chat/permissionSet.json | 73 + .../tools/atmo/chat/realtime/subscribe.json | 42 + .../tools/atmo/chat/realtime/ticket.json | 64 + .../atmo/chat/server}/getRecord.json | 20 +- .../tools/atmo/chat/server/listRecords.json | 251 ++ .../tools/atmo/chat/space/addMember.json | 52 + .../tools/atmo/chat/space/createSpace.json | 55 + .../tools/atmo/chat/space/defs.json | 210 ++ .../tools/atmo/chat/space/deleteRecord.json | 56 + .../tools/atmo/chat/space/getBlob.json | 42 + .../tools/atmo/chat/space/getRecord.json | 63 + .../tools/atmo/chat/space/getSpace.json | 49 + .../tools/atmo/chat/space/invite/create.json | 76 + .../tools/atmo/chat/space/invite/list.json | 52 + .../tools/atmo/chat/space/invite/redeem.json | 44 + .../tools/atmo/chat/space/invite/revoke.json | 51 + .../tools/atmo/chat/space/leaveSpace.json | 48 + .../tools/atmo/chat/space/listBlobs.json | 57 + .../tools/atmo/chat/space/listMembers.json | 48 + .../tools/atmo/chat/space/listRecords.json | 74 + .../tools/atmo/chat/space/listSpaces.json | 62 + .../tools/atmo/chat/space/putRecord.json | 68 + .../tools/atmo/chat/space/removeMember.json | 55 + .../tools/atmo/chat/space/uploadBlob.json | 50 + .../tools/atmo/chat/space/whoami.json | 45 + .../lexicons-pulled/README.md | 2 +- .../xyz/statusphere/status.json | 27 - .../lexicons/tools/atmo/chat/channel.json | 24 + .../lexicons/tools/atmo/chat/message.json | 19 + .../lexicons/tools/atmo/chat/server.json | 25 + examples/sveltekit-group-chat/package.json | 4 +- .../scripts/append-scheduled.ts | 15 +- .../sveltekit-group-chat/scripts/generate.ts | 31 +- examples/sveltekit-group-chat/src/app.d.ts | 7 + .../sveltekit-group-chat/src/hooks.server.ts | 1 + .../src/lib/atproto/generated-methods.ts | 58 + .../src/lib/atproto/server/repo.remote.ts | 9 +- .../src/lib/atproto/server/signed-cookie.ts | 37 + .../src/lib/atproto/settings.ts | 26 +- .../src/lib/contrail/config.ts | 27 +- .../src/lib/contrail/index.ts | 131 +- .../src/lib/rooms/invites.remote.ts | 65 + .../src/lib/rooms/profiles.svelte.ts | 70 + .../src/lib/rooms/realtime.svelte.ts | 216 ++ .../src/lib/rooms/rooms.remote.ts | 218 ++ .../src/lib/rooms/server.ts | 340 ++ .../src/lib/rooms/unread.svelte.ts | 72 + .../sveltekit-group-chat/src/lib/rooms/uri.ts | 23 + .../src/lib/rooms/watch.svelte.ts | 56 + .../src/routes/+layout.server.ts | 36 +- .../src/routes/+layout.svelte | 27 +- .../src/routes/+page.server.ts | 88 +- .../src/routes/+page.svelte | 228 +- .../src/routes/api/blob-upload/+server.ts | 23 + .../src/routes/api/blob/+server.ts | 46 + .../src/routes/api/cron/+server.ts | 7 +- .../src/routes/api/ticket/+server.ts | 24 + .../routes/c/[communityDid]/+layout.server.ts | 130 + .../routes/c/[communityDid]/+layout.svelte | 269 ++ .../src/routes/c/[communityDid]/+page.svelte | 27 + .../[channelKey]/+page.server.ts | 10 + .../[communityDid]/[channelKey]/+page.svelte | 144 + .../settings/invites/+page.server.ts | 50 + .../settings/invites/+page.svelte | 154 + .../settings/members/+page.server.ts | 49 + .../settings/members/+page.svelte | 142 + .../src/routes/join/[token]/+page.svelte | 56 + .../src/routes/new/+page.svelte | 155 + .../src/routes/xrpc/[...path]/+server.ts | 9 +- examples/sveltekit-group-chat/static/og.png | Bin 61667 -> 0 bytes examples/sveltekit-group-chat/wrangler.jsonc | 45 +- package.json | 4 + pnpm-lock.yaml | 3126 ++++++++++++++++- pnpm-workspace.yaml | 3 + src/core/community/adapter.ts | 115 +- src/core/community/index.ts | 2 + src/core/community/plc.ts | 48 +- src/core/community/router.ts | 164 + src/core/community/schema.ts | 14 + src/core/community/types.ts | 26 + src/core/realtime/durable-object.ts | 315 +- src/core/realtime/publishing-adapter.ts | 5 + src/core/realtime/query-filter.ts | 235 ++ src/core/realtime/ticket.ts | 18 +- src/core/router/collection.ts | 563 ++- src/core/router/index.ts | 8 +- src/core/spaces/auth.ts | 56 +- src/core/spaces/router.ts | 5 +- src/core/spaces/types.ts | 18 + src/generate.ts | 34 + src/index.ts | 41 + src/sync/index.ts | 403 +++ tsup.config.ts | 1 + 133 files changed, 12059 insertions(+), 516 deletions(-) create mode 100644 community-lexicon-templates/invite/create.json create mode 100644 community-lexicon-templates/invite/list.json create mode 100644 community-lexicon-templates/invite/redeem.json create mode 100644 community-lexicon-templates/invite/revoke.json delete mode 100644 examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json rename examples/sveltekit-group-chat/lexicons-generated/{statusphere/app => tools/atmo/chat}/getCursor.json (93%) rename examples/sveltekit-group-chat/lexicons-generated/{statusphere/app => tools/atmo/chat}/getOverview.json (96%) rename examples/sveltekit-group-chat/lexicons-generated/{statusphere/app => tools/atmo/chat}/getProfile.json (98%) create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json rename examples/sveltekit-group-chat/lexicons-generated/{statusphere/app/status => tools/atmo/chat/message}/listRecords.json (80%) rename examples/sveltekit-group-chat/lexicons-generated/{statusphere/app => tools/atmo/chat}/notifyOfUpdate.json (97%) create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json rename examples/sveltekit-group-chat/lexicons-generated/{statusphere/app/status => tools/atmo/chat/server}/getRecord.json (83%) create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json delete mode 100644 examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json create mode 100644 examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json create mode 100644 examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json create mode 100644 examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json create mode 100644 examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/server.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/uri.ts create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts create mode 100644 examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/api/blob/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts create mode 100644 examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte create mode 100644 examples/sveltekit-group-chat/src/routes/new/+page.svelte delete mode 100644 examples/sveltekit-group-chat/static/og.png create mode 100644 src/core/realtime/query-filter.ts create mode 100644 src/sync/index.ts diff --git a/community-lexicon-templates/defs.json b/community-lexicon-templates/defs.json index 680d394..5ba26f1 100644 --- a/community-lexicon-templates/defs.json +++ b/community-lexicon-templates/defs.json @@ -55,6 +55,22 @@ "did": { "type": "string", "format": "did" }, "addedAt": { "type": "integer" } } + }, + "inviteView": { + "type": "object", + "required": ["tokenHash", "spaceUri", "accessLevel", "createdBy", "createdAt", "usedCount"], + "properties": { + "tokenHash": { "type": "string", "description": "SHA-256 of the raw token. Stable id for list/revoke; never grants access on its own." }, + "spaceUri": { "type": "string", "format": "at-uri" }, + "accessLevel": { "type": "ref", "ref": "#accessLevel" }, + "createdBy": { "type": "string", "format": "did" }, + "createdAt": { "type": "integer" }, + "expiresAt": { "type": "integer", "description": "Unix ms. Omitted for no expiry." }, + "maxUses": { "type": "integer", "description": "Omitted for unlimited." }, + "usedCount": { "type": "integer" }, + "revokedAt": { "type": "integer", "description": "Unix ms. Omitted if not revoked." }, + "note": { "type": "string" } + } } } } diff --git a/community-lexicon-templates/invite/create.json b/community-lexicon-templates/invite/create.json new file mode 100644 index 0000000..de9a4d4 --- /dev/null +++ b/community-lexicon-templates/invite/create.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.invite.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "accessLevel"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, + "expiresAt": { "type": "integer", "description": "Unix ms. Omit for no expiry." }, + "maxUses": { "type": "integer", "minimum": 1, "description": "Omit for unlimited redemptions." }, + "note": { "type": "string", "maxLength": 500 } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["token", "tokenHash", "spaceUri", "accessLevel", "createdAt"], + "properties": { + "token": { "type": "string", "description": "Raw token. Shown once — cannot be retrieved later." }, + "tokenHash": { "type": "string" }, + "spaceUri": { "type": "string", "format": "at-uri" }, + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, + "expiresAt": { "type": "integer" }, + "maxUses": { "type": "integer" }, + "createdAt": { "type": "integer" } + } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "Forbidden" }, + { "name": "NotFound" } + ] + } + } +} diff --git a/community-lexicon-templates/invite/list.json b/community-lexicon-templates/invite/list.json new file mode 100644 index 0000000..4ee794f --- /dev/null +++ b/community-lexicon-templates/invite/list.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.invite.list", + "defs": { + "main": { + "type": "query", + "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", + "parameters": { + "type": "params", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "includeRevoked": { "type": "boolean", "default": false } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["invites"], + "properties": { + "invites": { + "type": "array", + "items": { "type": "ref", "ref": "tools.atmo.community.defs#inviteView" } + } + } + } + }, + "errors": [ + { "name": "Forbidden" }, + { "name": "NotFound" } + ] + } + } +} diff --git a/community-lexicon-templates/invite/redeem.json b/community-lexicon-templates/invite/redeem.json new file mode 100644 index 0000000..5935aa0 --- /dev/null +++ b/community-lexicon-templates/invite/redeem.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.invite.redeem", + "defs": { + "main": { + "type": "procedure", + "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["token"], + "properties": { + "token": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "accessLevel", "communityDid"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, + "communityDid": { "type": "string", "format": "did" } + } + } + }, + "errors": [ + { "name": "InvalidRequest" }, + { "name": "InvalidToken" }, + { "name": "NotFound" } + ] + } + } +} diff --git a/community-lexicon-templates/invite/revoke.json b/community-lexicon-templates/invite/revoke.json new file mode 100644 index 0000000..05b3054 --- /dev/null +++ b/community-lexicon-templates/invite/revoke.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "tools.atmo.community.invite.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["tokenHash"], + "properties": { + "tokenHash": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { + "ok": { "type": "boolean" } + } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/docs/community-spec-mapping.md b/docs/community-spec-mapping.md index 2b1fd41..ba2cc1b 100644 --- a/docs/community-spec-mapping.md +++ b/docs/community-spec-mapping.md @@ -36,7 +36,7 @@ permissioned-data spec with no knowledge of access levels or delegation. | Owner | `owner`; only meaningful in `$admin` for owner-management | ✅ | Post's level 8 | | Push model for membership lists | Reconciler writes `spaces_members` after each change | ➕ | Post doesn't specify a sync direction; push keeps spaces read-path zero-overhead | | Cross-community / cross-arbiter delegation | _same-contrail only_ | ❌ | v1 constraint. Private-membership federation is deferred — see [community.md § Deferred work] | -| Invites as a separate service | _not implemented_ | ❌ | Post proposes an optional add-on; contrail has spaces invites to draw from when this lands | +| Invites as a separate service | `community.invite.*` (create/list/revoke/redeem) | ➕ | Built-in — tokens pre-sign a grant, redeemer gets the encoded access level. SHA-256 at rest, atomic redeem. | | Writing records under the arbiter's account | `community.space.putRecord` (in-space) + `community.putRecord` (public) | ✅ | In-space: `admin+`. Public: `member+` in `$publishers` — routes through adopted community's PDS | | Public membership-list flag | _none_ | ❌ | Post allows spaces to expose membership publicly. Ruled out for cross-instance federation (privacy) | | Space credential (from arbiter) | _none_ | ⚠️ | Covered by the spaces module's service-auth story, not the community module | @@ -64,6 +64,9 @@ membership is user-controlled or community-controlled. - `community.space.grant` · `community.space.revoke` · `community.space.setAccessLevel` - `community.space.listMembers` (`?flatten=true` for the resolved DID list) · `community.space.resync` +### Invites +- `community.invite.create` · `community.invite.list` · `community.invite.revoke` · `community.invite.redeem` + ### Publishing - `community.putRecord` · `community.deleteRecord` (public records via the community's PDS; adopted only) - `community.space.putRecord` · `community.space.deleteRecord` (in-space records authored by the community DID) diff --git a/examples/sveltekit-group-chat/.env.example b/examples/sveltekit-group-chat/.env.example index ede6fba..865a865 100644 --- a/examples/sveltekit-group-chat/.env.example +++ b/examples/sveltekit-group-chat/.env.example @@ -4,3 +4,19 @@ COOKIE_SECRET= # Set to your tunnel URL to use a confidential client in dev # OAUTH_PUBLIC_URL=https://your-tunnel.trycloudflare.com + +# 32-byte base64 secret used to envelope-encrypt community signing keys. +# Generate with: openssl rand -base64 32 +COMMUNITY_MASTER_KEY= + +# 32-byte base64 secret used to HMAC-sign realtime subscription tickets. +# Generate with: openssl rand -base64 32 +REALTIME_TICKET_SECRET= + +# Service DID for JWT verification (aud claim). did:web: for prod. +SERVICE_DID=did:web:localhost + +# Dev-only: trust the HMAC-signed session cookie instead of atproto service-auth +# JWTs, so the loopback OAuth client can reach contrail without a tunnel. NEVER +# set this in production — it turns the cookie into the only auth signal. +DEV_AUTH=1 diff --git a/examples/sveltekit-group-chat/README.md b/examples/sveltekit-group-chat/README.md index 4917318..07f807c 100644 --- a/examples/sveltekit-group-chat/README.md +++ b/examples/sveltekit-group-chat/README.md @@ -1,76 +1,116 @@ -# sveltekit + contrail + atproto OAuth +# sveltekit-group-chat -A sveltekit example app on cloudflare workers with self-hosted atproto record indexing via [contrail](https://github.com/flo-bit/contrail), fully typed queries, and OAuth authentication. +A Discord-like demo built on top of [contrail](https://github.com/flo-bit/contrail) — SvelteKit + Cloudflare Workers + D1, OAuth, spaces, communities, and realtime (SSE). + +## What this exercises + +- **`community`** — mint a fresh `did:plc` per server, tiered access levels (member / manager / admin / owner), space → space delegation. +- **`spaces`** — permissioned per-channel record tables, membership pushed in by the community reconciler. +- **`realtime`** — single SSE connection subscribed to `community:`, fanning out per-channel events with unread dots derived client-side. +- Three record collections — `tools.atmo.chat.server`, `tools.atmo.chat.channel`, `tools.atmo.chat.message` — with unified `listRecords` for "every server / channel I can see" in one call. ## Setup ```sh pnpm install +cp .env.example .env +# fill in COMMUNITY_MASTER_KEY and REALTIME_TICKET_SECRET with +# openssl rand -base64 32 +pnpm generate:pull # emits lexicons + src/lib/atproto/generated-methods.ts pnpm dev ``` -Dev mode uses a loopback OAuth client — no keys or Cloudflare setup needed. +Re-run `pnpm generate` whenever you add/remove collections or toggle contrail +modules in `src/lib/contrail/config.ts`: the OAuth scope list is derived from +it, so the consent screen only asks for permissions you actually use. -## Config +Dev mode uses a loopback OAuth client — no Cloudflare setup needed. The realtime Durable Object runs locally via miniflare. -Define which AT Protocol collections to index in `src/lib/contrail/config.ts`: +### Auth in dev vs prod -```ts -import type { ContrailConfig } from '@atmo-dev/contrail'; +Contrail normally auths every write with an atproto **service-auth JWT**. The browser delegates minting those JWTs to the SvelteKit worker, which calls `com.atproto.server.getServiceAuth` on the user's PDS. -export const config: ContrailConfig = { - namespace: 'statusphere.app', - collections: { - status: { // short name → URL path segment - collection: 'xyz.statusphere.status', // full NSID of the record type - queryable: { - status: {}, // equality filter (?status=...) - createdAt: { type: 'range' } // range filter (?createdAtMin=...&createdAtMax=...) - } - } - } -}; -``` +bsky.social **refuses** that call for loopback OAuth clients, so out of the box a plain `pnpm dev` couldn't talk to contrail without a [cloudflared tunnel](#with-a-tunnel). -After changing the config, run `pnpm generate:pull` to regenerate lexicons and types. +To skip the tunnel this example ships a **dev auth bypass** gated on `DEV_AUTH=1` in `.env`: -Run `pnpm sync` to backfill existing records from the network. +- Contrail's auth middleware trusts the HMAC-signed `did` session cookie the OAuth flow already sets, and uses its DID as the authenticated caller. +- The cookie is HMAC-signed with `COOKIE_SECRET`, so only this worker can mint a valid one. +- All access-level checks (admin+, manager+, etc.) still apply — the bypass only replaces the JWT step. -Wrangler bindings (`wrangler.jsonc`): +**Never set `DEV_AUTH=1` in production** — that would make the cookie the sole auth signal for third-party clients. -- **D1** (`DB`) — contrail's database -- **KV** (`OAUTH_SESSIONS`, `OAUTH_STATES`) — OAuth session storage -- **Vars** — `CRON_SECRET`, `OAUTH_PUBLIC_URL`, `CLIENT_ASSERTION_KEY`, `COOKIE_SECRET` +### With a tunnel -## Deploy +If you want to exercise the real JWT path in dev, run a tunnel and drop `DEV_AUTH`: ```sh -npx wrangler d1 create statusphere -# Add database_id to wrangler.jsonc +pnpm tunnel # prints https://xxx.trycloudflare.com +# in .env: set OAUTH_PUBLIC_URL to the tunnel URL and remove DEV_AUTH +pnpm dev +# open the tunnel URL (not localhost) +``` + +## How the pieces connect -pnpm build -npx wrangler deploy ``` +┌────────────┐ service-auth JWT ┌─────────────────┐ contrail XRPCs ┌───────────┐ +│ browser │ ─────────────────► │ SvelteKit worker │ ─────────────────► │ contrail │ +└────────────┘ └─────────────────┘ └───────────┘ + │ │ │ + │ EventSource /xrpc/.../realtime.subscribe?ticket=... ◄──────── realtime DO │ + └───────────────────────────────────────────────────────────────────────────┘ +``` + +- The browser can't mint service-auth JWTs itself — it delegates to the SvelteKit worker, which uses the user's OAuth session to call `com.atproto.server.getServiceAuth` on their PDS. The resulting JWT is scoped to one `lxm`. +- Realtime tickets are minted the same way, then the browser holds an `EventSource` open against the subscribe endpoint. -## How it works +## Data model -**Contrail** indexes AT Protocol records into D1 via Jetstream (cron, every minute). When a user posts, `contrail.notify()` indexes it immediately. +| Record | Lives in | Author | +|---|---|---| +| `tools.atmo.chat.server` | community's `members` space, rkey `self` | community DID (via `community.space.putRecord`) | +| `tools.atmo.chat.channel` | channel's own space, rkey `self` | community DID | +| `tools.atmo.chat.message` | channel's own space, rkey = TID | user DID | -**Typed queries** use `@atcute/client` with an in-process handler — full type safety, zero HTTP overhead: +Only admins (`admin+` in the target space) can write the server and channel records, and the community DID is the author. Reads filter by `actor=` so spoof records from random members are silently ignored. -```ts -const client = getClient(platform!.env.DB); -const res = await client.get('statusphere.app.status.listRecords', { - params: { limit: 50, profiles: true } // typed params -}); -res.data.records // typed response +## Feature map + +| Route | What it does | +|---|---| +| `/` | "My servers" — unified `server.listRecords` over every space I'm in | +| `/new` | Mint community, bootstrap `members` role-space, write server record, show recovery key once | +| `/c/[communityDid]` | Redirects to the first channel you can see | +| `/c/[communityDid]/[channelKey]` | Chat view — messages via `space.listRecords` + realtime | +| `/c/[communityDid]/settings/members` | Add, promote, demote, revoke members | + +## Private channels + +New-channel modal offers a visibility toggle: + +- **Public** — grants the `members` role-space `member` access on the new channel. Reconciler fans out to everyone. +- **Private** — grants each picked DID directly. Also grants `members` (so they can see the server header) but not automatic access to other channels. + +## Deploy + +```sh +npx wrangler d1 create group-chat +# add the database_id to wrangler.jsonc +npx wrangler secret put COMMUNITY_MASTER_KEY +npx wrangler secret put REALTIME_TICKET_SECRET + +pnpm build +npx wrangler deploy ``` -Types are generated from contrail's config via `pnpm generate:pull`, which produces lexicon JSON and TypeScript types that register with `@atcute/client`. +## Known gaps -**Scheduled ingestion** works around SvelteKit's lack of `scheduled` export support ([sveltejs/kit#4841](https://github.com/sveltejs/kit/issues/4841)) by appending a handler post-build that self-calls `/api/cron`. +This is a demo. It deliberately skips: -**OAuth** uses `@atcute/oauth-node-client` with KV-backed sessions and HMAC-signed cookies. +- **`adopt` mode** — only `mint` is wired up. +- **`$publishers`** / community-authored public posts. +- Threads, reactions, image uploads, rich text, typing indicators. ## License diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json b/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json deleted file mode 100644 index 232cfee..0000000 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/permissionSet.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "statusphere.app.permissionSet", - "defs": { - "main": { - "type": "permission-set", - "title": "statusphere.app", - "description": "All XRPC methods exposed by the statusphere.app service.", - "permissions": [ - { - "type": "permission", - "resource": "rpc", - "inheritAud": true, - "lxm": [ - "statusphere.app.getCursor", - "statusphere.app.getOverview", - "statusphere.app.getProfile", - "statusphere.app.notifyOfUpdate", - "statusphere.app.status.getRecord", - "statusphere.app.status.listRecords" - ] - } - ] - } - } -} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json new file mode 100644 index 0000000..63ce636 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json @@ -0,0 +1,184 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.channel.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single tools.atmo.chat.channel record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "tools.atmo.chat.channel#main" + }, + "time_us": { + "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json new file mode 100644 index 0000000..3a9d098 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json @@ -0,0 +1,256 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.channel.listRecords", + "defs": { + "main": { + "type": "query", + "description": "Query tools.atmo.chat.channel records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + }, + "communityDid": { + "type": "string", + "description": "Filter by communityDid" + }, + "name": { + "type": "string", + "description": "Filter by name" + }, + "topic": { + "type": "string", + "description": "Filter by topic" + }, + "visibility": { + "type": "string", + "description": "Filter by visibility" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "sort": { + "type": "string", + "knownValues": [ + "communityDid", + "name", + "topic", + "visibility", + "createdAt" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "tools.atmo.chat.channel#main" + }, + "time_us": { + "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json new file mode 100644 index 0000000..6f73301 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.adopt", + "defs": { + "main": { + "type": "procedure", + "description": "Adopt an existing ATProto account as a community identity. The provided app password is verified by creating a session, then stored encrypted. Creates the reserved `$admin` and `$publishers` spaces with the caller as `owner` in both.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "identifier", + "appPassword" + ], + "properties": { + "identifier": { + "type": "string", + "description": "Handle or DID of the account to adopt." + }, + "appPassword": { + "type": "string", + "description": "App password for the account; verified by createSession and stored encrypted." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "Unauthorized" + }, + { + "name": "AlreadyExists" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json new file mode 100644 index 0000000..8555c23 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json @@ -0,0 +1,185 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.defs", + "description": "Shared types for community-module XRPC methods.", + "defs": { + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "Module-internal access levels that govern community operations on a given space. Ordered: member < manager < admin < owner." + }, + "communityView": { + "type": "object", + "required": [ + "did", + "mode", + "createdAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "mode": { + "type": "string", + "knownValues": [ + "adopt", + "mint" + ] + }, + "identifier": { + "type": "string", + "description": "Handle or DID at adoption time (adopt only)." + }, + "createdAt": { + "type": "integer" + } + } + }, + "spaceView": { + "type": "object", + "required": [ + "uri", + "ownerDid", + "type", + "key", + "serviceDid", + "createdAt" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "ownerDid": { + "type": "string", + "format": "did" + }, + "type": { + "type": "string", + "format": "nsid" + }, + "key": { + "type": "string" + }, + "serviceDid": { + "type": "string" + }, + "createdAt": { + "type": "integer" + } + } + }, + "subject": { + "type": "object", + "description": "Exactly one of `did` or `spaceUri` must be set.", + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "memberRow": { + "type": "object", + "required": [ + "subject", + "accessLevel", + "grantedBy", + "grantedAt" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "#subject" + }, + "accessLevel": { + "type": "ref", + "ref": "#accessLevel" + }, + "grantedBy": { + "type": "string", + "format": "did" + }, + "grantedAt": { + "type": "integer" + } + } + }, + "flatMember": { + "type": "object", + "required": [ + "did", + "addedAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "addedAt": { + "type": "integer" + } + } + }, + "inviteView": { + "type": "object", + "required": [ + "tokenHash", + "spaceUri", + "accessLevel", + "createdBy", + "createdAt", + "usedCount" + ], + "properties": { + "tokenHash": { + "type": "string", + "description": "SHA-256 of the raw token. Stable id for list/revoke; never grants access on its own." + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "#accessLevel" + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms. Omitted for no expiry." + }, + "maxUses": { + "type": "integer", + "description": "Omitted for unlimited." + }, + "usedCount": { + "type": "integer" + }, + "revokedAt": { + "type": "integer", + "description": "Unix ms. Omitted if not revoked." + }, + "note": { + "type": "string" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json new file mode 100644 index 0000000..a8d7a1b --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.delete", + "defs": { + "main": { + "type": "procedure", + "description": "Soft-delete a community. Also soft-deletes all spaces owned by the community. Caller must have `owner` in the community's `$admin` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json new file mode 100644 index 0000000..a2dbfe4 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json @@ -0,0 +1,65 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a public record authored by the community from the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "collection", + "rkey" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "NotSupported" + }, + { + "name": "UpstreamFailure" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json new file mode 100644 index 0000000..0122d19 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.getHealth", + "defs": { + "main": { + "type": "query", + "description": "Check whether stored credentials for the community are still usable. Adopted: attempts a live session creation. Minted: verifies the signing key can be decrypted. Caller must have at least `member` in the community's `$admin` space.", + "parameters": { + "type": "params", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "knownValues": [ + "healthy", + "degraded", + "expired" + ] + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json new file mode 100644 index 0000000..9babcc7 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json @@ -0,0 +1,93 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.invite.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "accessLevel" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#accessLevel" + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms. Omit for no expiry." + }, + "maxUses": { + "type": "integer", + "minimum": 1, + "description": "Omit for unlimited redemptions." + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token", + "tokenHash", + "spaceUri", + "accessLevel", + "createdAt" + ], + "properties": { + "token": { + "type": "string", + "description": "Raw token. Shown once — cannot be retrieved later." + }, + "tokenHash": { + "type": "string" + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#accessLevel" + }, + "expiresAt": { + "type": "integer" + }, + "maxUses": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "Forbidden" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json new file mode 100644 index 0000000..55ef50a --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.invite.list", + "defs": { + "main": { + "type": "query", + "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "includeRevoked": { + "type": "boolean", + "default": false + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "invites" + ], + "properties": { + "invites": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#inviteView" + } + } + } + } + }, + "errors": [ + { + "name": "Forbidden" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json new file mode 100644 index 0000000..bc83ece --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.invite.redeem", + "defs": { + "main": { + "type": "procedure", + "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "accessLevel", + "communityDid" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#accessLevel" + }, + "communityDid": { + "type": "string", + "format": "did" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "InvalidToken" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json new file mode 100644 index 0000000..30f2cf0 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.invite.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "tokenHash" + ], + "properties": { + "tokenHash": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json new file mode 100644 index 0000000..cc8d1ae --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json @@ -0,0 +1,38 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.list", + "defs": { + "main": { + "type": "query", + "description": "List communities where the actor has any access level in any community-owned space. Defaults to the JWT issuer when `actor` is omitted.", + "parameters": { + "type": "params", + "properties": { + "actor": { + "type": "string", + "format": "did", + "description": "DID to query (defaults to the JWT issuer)." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communities" + ], + "properties": { + "communities": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#communityView" + } + } + } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json new file mode 100644 index 0000000..399cc97 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.mint", + "defs": { + "main": { + "type": "procedure", + "description": "Mint a fresh did:plc for a new community. Contrail generates three P-256 keypairs (signing, contrail rotation, creator rotation), submits a genesis op to the PLC directory, and stores the signing + contrail-rotation keys encrypted. The creator's rotation key is returned once as a recovery secret and never stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "handle": { + "type": "string", + "description": "Optional handle to include in alsoKnownAs (as at://)." + }, + "pdsEndpoint": { + "type": "string", + "description": "Optional PDS endpoint to include as the atproto_pds service." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "recoveryKey" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "recoveryKey": { + "type": "unknown", + "description": "Creator's rotation key as a private JWK. Shown once — cannot be retrieved later." + } + } + } + }, + "errors": [ + { + "name": "UpstreamFailure" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json new file mode 100644 index 0000000..ead6778 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json @@ -0,0 +1,73 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Publish a public record authored by the community via the community's PDS. Adopted communities only. Caller must be `member` or higher in the community's `$publishers` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "collection", + "record" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "record": { + "type": "unknown" + }, + "validate": { + "type": "boolean" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "NotSupported" + }, + { + "name": "UpstreamFailure" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json new file mode 100644 index 0000000..a4e4fa3 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.reauth", + "defs": { + "main": { + "type": "procedure", + "description": "Replace the stored app password for an adopted community. Caller must have `owner` access in the community's `$admin` space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid", + "appPassword" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "appPassword": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "Unauthorized" + }, + { + "name": "NotSupported" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json new file mode 100644 index 0000000..611d925 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json @@ -0,0 +1,58 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create a community-owned space (group/role/channel). Caller needs `admin` or higher in the community's `$admin` space. Reserved keys (`$admin`, `$publishers`, …) are rejected here.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "communityDid" + ], + "properties": { + "communityDid": { + "type": "string", + "format": "did" + }, + "key": { + "type": "string", + "description": "Space key. Auto-generated if omitted." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "space" + ], + "properties": { + "space": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#spaceView" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "AlreadyExists" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json new file mode 100644 index 0000000..3754ec4 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.delete", + "defs": { + "main": { + "type": "procedure", + "description": "Soft-delete a community-owned space. Caller must be `owner` on the space, or `admin`+ in the community's `$admin` space. Reserved spaces cannot be deleted.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json new file mode 100644 index 0000000..19bce03 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json @@ -0,0 +1,59 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete an in-space record authored by the community DID. Caller must be `admin` or higher in the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "rkey" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json new file mode 100644 index 0000000..b167e77 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.grant", + "defs": { + "main": { + "type": "procedure", + "description": "Grant or upsert a subject's access level on a space. Caller needs `manager` or higher; the granted level cannot exceed the caller's own. A subject can be a DID (member) or another space (delegated membership).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "subject", + "accessLevel" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#subject" + }, + "accessLevel": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#accessLevel" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json new file mode 100644 index 0000000..f9b8a33 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json @@ -0,0 +1,58 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.listMembers", + "defs": { + "main": { + "type": "query", + "description": "List members of a community-owned space. By default returns the raw access-level rows; with `flatten=true` returns the flattened DID list (delegated memberships resolved).", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "flatten": { + "type": "boolean", + "description": "If true, return the flat DID list instead of raw rows." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#memberRow" + }, + "description": "Present when flatten=false." + }, + "members": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#flatMember" + }, + "description": "Present when flatten=true." + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json new file mode 100644 index 0000000..016f414 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json @@ -0,0 +1,71 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Write an in-space record authored by the community DID (rather than the caller). Caller must be `admin` or higher in the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "record" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "record": { + "type": "unknown" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "rkey", + "authorDid", + "createdAt" + ], + "properties": { + "rkey": { + "type": "string" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json new file mode 100644 index 0000000..06aa3c5 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.resync", + "defs": { + "main": { + "type": "procedure", + "description": "Manually recompute `spaces_members` for a community-owned space. Useful after a crash mid-reconcile leaves stale membership. Caller must have `admin` or higher.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json new file mode 100644 index 0000000..6e15c4c --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Remove a subject's access grant from a space. Caller must have `manager` or higher and outrank the subject's current level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "subject" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#subject" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json new file mode 100644 index 0000000..0c2bb94 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.space.setAccessLevel", + "defs": { + "main": { + "type": "procedure", + "description": "Change the access level of an existing grant. Caller must outrank both the old and new level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "subject", + "accessLevel" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#subject" + }, + "accessLevel": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#accessLevel" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json new file mode 100644 index 0000000..92b1f97 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.community.whoami", + "defs": { + "main": { + "type": "query", + "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "tools.atmo.chat.community.defs#accessLevel", + "description": "Null when caller has no resolvable access." + } + } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getCursor.json similarity index 93% rename from examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getCursor.json index f85bdbf..59418d2 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getCursor.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getCursor.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "statusphere.app.getCursor", + "id": "tools.atmo.chat.getCursor", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getOverview.json similarity index 96% rename from examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getOverview.json index 67dd2f5..e391c82 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getOverview.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getOverview.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "statusphere.app.getOverview", + "id": "tools.atmo.chat.getOverview", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getProfile.json similarity index 98% rename from examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getProfile.json index 4706150..ee61ec6 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/getProfile.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getProfile.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "statusphere.app.getProfile", + "id": "tools.atmo.chat.getProfile", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json new file mode 100644 index 0000000..aefe0d3 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json @@ -0,0 +1,184 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.message.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single tools.atmo.chat.message record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "tools.atmo.chat.message#main" + }, + "time_us": { + "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json similarity index 80% rename from examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json index 8fb9309..2aa24f3 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/listRecords.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "statusphere.app.status.listRecords", + "id": "tools.atmo.chat.message.listRecords", "defs": { "main": { "type": "query", - "description": "Query xyz.statusphere.status records with filters", + "description": "Query tools.atmo.chat.message records with filters", "parameters": { "type": "params", "properties": { @@ -26,9 +26,23 @@ "type": "boolean", "description": "Include profile + identity info keyed by DID" }, - "status": { + "spaceUri": { "type": "string", - "description": "Filter by status" + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + }, + "text": { + "type": "string", + "description": "Filter by text" }, "createdAtMin": { "type": "string", @@ -38,11 +52,16 @@ "type": "string", "description": "Maximum value for createdAt" }, + "replyTo": { + "type": "string", + "description": "Filter by replyTo" + }, "sort": { "type": "string", "knownValues": [ - "status", - "createdAt" + "text", + "createdAt", + "replyTo" ], "description": "Field to sort by (default: time_us)" }, @@ -115,10 +134,15 @@ }, "record": { "type": "ref", - "ref": "xyz.statusphere.status#main" + "ref": "tools.atmo.chat.message#main" }, "time_us": { "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." } } }, diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json similarity index 97% rename from examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json index f34ded3..7dc40fa 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/notifyOfUpdate.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "statusphere.app.notifyOfUpdate", + "id": "tools.atmo.chat.notifyOfUpdate", "defs": { "main": { "type": "procedure", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json new file mode 100644 index 0000000..1704d8a --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json @@ -0,0 +1,73 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.permissionSet", + "defs": { + "main": { + "type": "permission-set", + "title": "tools.atmo.chat", + "description": "All XRPC methods exposed by the tools.atmo.chat service.", + "permissions": [ + { + "type": "permission", + "resource": "rpc", + "aud": "*", + "lxm": [ + "tools.atmo.chat.channel.getRecord", + "tools.atmo.chat.channel.listRecords", + "tools.atmo.chat.community.adopt", + "tools.atmo.chat.community.delete", + "tools.atmo.chat.community.deleteRecord", + "tools.atmo.chat.community.getHealth", + "tools.atmo.chat.community.invite.create", + "tools.atmo.chat.community.invite.list", + "tools.atmo.chat.community.invite.redeem", + "tools.atmo.chat.community.invite.revoke", + "tools.atmo.chat.community.list", + "tools.atmo.chat.community.mint", + "tools.atmo.chat.community.putRecord", + "tools.atmo.chat.community.reauth", + "tools.atmo.chat.community.space.create", + "tools.atmo.chat.community.space.delete", + "tools.atmo.chat.community.space.deleteRecord", + "tools.atmo.chat.community.space.grant", + "tools.atmo.chat.community.space.listMembers", + "tools.atmo.chat.community.space.putRecord", + "tools.atmo.chat.community.space.resync", + "tools.atmo.chat.community.space.revoke", + "tools.atmo.chat.community.space.setAccessLevel", + "tools.atmo.chat.community.whoami", + "tools.atmo.chat.getCursor", + "tools.atmo.chat.getOverview", + "tools.atmo.chat.getProfile", + "tools.atmo.chat.message.getRecord", + "tools.atmo.chat.message.listRecords", + "tools.atmo.chat.notifyOfUpdate", + "tools.atmo.chat.realtime.subscribe", + "tools.atmo.chat.realtime.ticket", + "tools.atmo.chat.server.getRecord", + "tools.atmo.chat.server.listRecords", + "tools.atmo.chat.space.addMember", + "tools.atmo.chat.space.createSpace", + "tools.atmo.chat.space.deleteRecord", + "tools.atmo.chat.space.getBlob", + "tools.atmo.chat.space.getRecord", + "tools.atmo.chat.space.getSpace", + "tools.atmo.chat.space.invite.create", + "tools.atmo.chat.space.invite.list", + "tools.atmo.chat.space.invite.redeem", + "tools.atmo.chat.space.invite.revoke", + "tools.atmo.chat.space.leaveSpace", + "tools.atmo.chat.space.listBlobs", + "tools.atmo.chat.space.listMembers", + "tools.atmo.chat.space.listRecords", + "tools.atmo.chat.space.listSpaces", + "tools.atmo.chat.space.putRecord", + "tools.atmo.chat.space.removeMember", + "tools.atmo.chat.space.uploadBlob", + "tools.atmo.chat.space.whoami" + ] + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json new file mode 100644 index 0000000..0e2b783 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.realtime.subscribe", + "defs": { + "main": { + "type": "query", + "description": "Open a streaming subscription. Responds with Server-Sent Events by default, or upgrades to WebSocket when the request carries `Upgrade: websocket`. Auth via `?ticket=` (browser) or `Authorization: Bearer ` (server-side). The stream closes with a final `member.removed` event if the caller loses access mid-stream.", + "parameters": { + "type": "params", + "properties": { + "ticket": { + "type": "string", + "description": "Subscription ticket from `.ticket`. Required if no Authorization header." + }, + "topic": { + "type": "string", + "description": "Optional: restrict the stream to a single topic (must be in the ticket). Required on the JWT path." + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "Optional: drop `record.*` events whose collection doesn't match." + } + } + }, + "errors": [ + { + "name": "AuthRequired" + }, + { + "name": "Forbidden" + }, + { + "name": "InvalidRequest" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json new file mode 100644 index 0000000..06d9868 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json @@ -0,0 +1,64 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.realtime.ticket", + "defs": { + "main": { + "type": "procedure", + "description": "Mint a short-lived subscription ticket for the given topic. The caller's access is checked and — for `community:` topics — expanded to the concrete list of `space:` topics the caller can see. Server-side callers may skip tickets and use their JWT directly on `.subscribe`.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "topic" + ], + "properties": { + "topic": { + "type": "string", + "description": "Requested topic. One of `space:`, `community:`, `actor:` (self-only in v1)." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ticket", + "topics", + "expiresAt" + ], + "properties": { + "ticket": { + "type": "string", + "description": "HMAC-signed opaque token. Passed as `?ticket=` on `.subscribe`." + }, + "topics": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Concrete delivery topics authorized by this ticket." + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms." + } + } + } + }, + "errors": [ + { + "name": "Forbidden" + }, + { + "name": "NotFound" + }, + { + "name": "NotSupported" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json similarity index 83% rename from examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json index da31528..81f345d 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/statusphere/app/status/getRecord.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "statusphere.app.status.getRecord", + "id": "tools.atmo.chat.server.getRecord", "defs": { "main": { "type": "query", - "description": "Get a single xyz.statusphere.status record by AT URI", + "description": "Get a single tools.atmo.chat.server record by AT URI", "parameters": { "type": "params", "required": [ @@ -19,6 +19,15 @@ "profiles": { "type": "boolean", "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, fetch from this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." } } }, @@ -54,11 +63,16 @@ }, "record": { "type": "ref", - "ref": "xyz.statusphere.status#main" + "ref": "tools.atmo.chat.server#main" }, "time_us": { "type": "integer" }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, "profiles": { "type": "array", "items": { diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json new file mode 100644 index 0000000..f7e057f --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json @@ -0,0 +1,251 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.server.listRecords", + "defs": { + "main": { + "type": "query", + "description": "Query tools.atmo.chat.server records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + }, + "communityDid": { + "type": "string", + "description": "Filter by communityDid" + }, + "name": { + "type": "string", + "description": "Filter by name" + }, + "description": { + "type": "string", + "description": "Filter by description" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "sort": { + "type": "string", + "knownValues": [ + "communityDid", + "name", + "description", + "createdAt" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "tools.atmo.chat.server#main" + }, + "time_us": { + "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "#appBskyActorProfile" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json new file mode 100644 index 0000000..59a3c55 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.addMember", + "defs": { + "main": { + "type": "procedure", + "description": "Add a member to a space. Caller must be the space owner.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "did" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json new file mode 100644 index 0000000..7894679 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.createSpace", + "defs": { + "main": { + "type": "procedure", + "description": "Create a new space owned by the JWT issuer. The caller is added as an owner-perm member.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "format": "nsid", + "description": "Space type NSID. Defaults to the service's configured type." + }, + "key": { + "type": "string", + "description": "Space key. Auto-generated (TID) if omitted." + }, + "appPolicyRef": { + "type": "string", + "format": "at-uri" + }, + "appPolicy": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#appPolicy" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "space" + ], + "properties": { + "space": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#spaceView" + } + } + } + }, + "errors": [ + { + "name": "AlreadyExists" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json new file mode 100644 index 0000000..884c91f --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json @@ -0,0 +1,210 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.defs", + "description": "Shared types for permissioned-space XRPC methods.", + "defs": { + "spaceView": { + "type": "object", + "required": [ + "uri", + "ownerDid", + "type", + "key", + "serviceDid", + "createdAt" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "ownerDid": { + "type": "string", + "format": "did" + }, + "type": { + "type": "string", + "format": "nsid" + }, + "key": { + "type": "string" + }, + "serviceDid": { + "type": "string" + }, + "appPolicyRef": { + "type": "string", + "format": "at-uri" + }, + "createdAt": { + "type": "integer" + }, + "appPolicy": { + "type": "ref", + "ref": "#appPolicy", + "description": "Owner-only" + } + } + }, + "memberView": { + "type": "object", + "required": [ + "did", + "addedAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "addedAt": { + "type": "integer" + }, + "addedBy": { + "type": "string", + "format": "did" + } + } + }, + "recordView": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "authorDid", + "rkey", + "record", + "createdAt" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "record": { + "type": "unknown" + }, + "createdAt": { + "type": "integer" + } + } + }, + "appPolicy": { + "type": "object", + "required": [ + "mode", + "apps" + ], + "properties": { + "mode": { + "type": "string", + "knownValues": [ + "allow", + "deny" + ], + "description": "'allow' = default-allow with apps[] as denylist; 'deny' = default-deny with apps[] as allowlist." + }, + "apps": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "blobInfo": { + "type": "object", + "required": [ + "cid", + "mimeType", + "size", + "authorDid", + "createdAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + } + } + }, + "inviteView": { + "type": "object", + "required": [ + "tokenHash", + "spaceUri", + "kind", + "usedCount", + "createdBy", + "createdAt" + ], + "properties": { + "tokenHash": { + "type": "string" + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "kind": { + "type": "string", + "knownValues": [ + "join", + "read", + "read-join" + ] + }, + "expiresAt": { + "type": "integer" + }, + "maxUses": { + "type": "integer" + }, + "usedCount": { + "type": "integer" + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + }, + "revokedAt": { + "type": "integer" + }, + "note": { + "type": "string" + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json new file mode 100644 index 0000000..926bc29 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a record from a space. Callers can delete their own records; the space owner can delete any record (via a separate admin path).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "rkey" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json new file mode 100644 index 0000000..ebda529 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.getBlob", + "defs": { + "main": { + "type": "query", + "description": "Read a blob from a space. Requires read access via a service-auth JWT or a read-grant invite token.", + "parameters": { + "type": "params", + "required": [ + "spaceUri", + "cid" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access." + } + } + }, + "output": { + "encoding": "*/*" + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json new file mode 100644 index 0000000..25deb0e --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json @@ -0,0 +1,63 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single record from a space.", + "parameters": { + "type": "params", + "required": [ + "spaceUri", + "collection", + "author", + "rkey" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "author": { + "type": "string", + "format": "did" + }, + "rkey": { + "type": "string" + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "record" + ], + "properties": { + "record": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#recordView" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json new file mode 100644 index 0000000..8e76e6e --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.getSpace", + "defs": { + "main": { + "type": "query", + "description": "Get metadata for a single space. Caller must be a member, the owner, or hold a read-grant invite token.", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "space" + ], + "properties": { + "space": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#spaceView" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json new file mode 100644 index 0000000..dc058ac --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json @@ -0,0 +1,76 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.invite.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create an invite for a space. Caller must be the space owner. Returns the raw token once; only the hash is stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "kind": { + "type": "string", + "knownValues": [ + "join", + "read", + "read-join" + ], + "default": "join", + "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms timestamp. Omit for no expiry." + }, + "maxUses": { + "type": "integer", + "minimum": 1, + "description": "Caps join redemptions only — read-token reads are unlimited. Omit for unlimited joins." + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token", + "invite" + ], + "properties": { + "token": { + "type": "string", + "description": "Raw token. Shown once — cannot be retrieved later." + }, + "invite": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#inviteView" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json new file mode 100644 index 0000000..aa3924f --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.invite.list", + "defs": { + "main": { + "type": "query", + "description": "List invites for a space. Owner only.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "includeRevoked": { + "type": "boolean", + "default": false + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "invites" + ], + "properties": { + "invites": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#inviteView" + } + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json new file mode 100644 index 0000000..8398704 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.invite.redeem", + "defs": { + "main": { + "type": "procedure", + "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "errors": [ + { + "name": "InvalidInvite" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json new file mode 100644 index 0000000..3ccbace --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.invite.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "tokenHash" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "tokenHash": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json new file mode 100644 index 0000000..d750d16 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json @@ -0,0 +1,48 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.leaveSpace", + "defs": { + "main": { + "type": "procedure", + "description": "Remove the caller from a space's member list. The owner cannot leave — they must delete the space instead.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "InvalidRequest", + "description": "Raised if the caller is the space owner." + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json new file mode 100644 index 0000000..76e2524 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.listBlobs", + "defs": { + "main": { + "type": "query", + "description": "List blob metadata for a space. Members only.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only blobs uploaded by this DID." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "blobs" + ], + "properties": { + "blobs": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#blobInfo" + } + }, + "cursor": { + "type": "string" + } + } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json new file mode 100644 index 0000000..c715baa --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json @@ -0,0 +1,48 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.listMembers", + "defs": { + "main": { + "type": "query", + "description": "List members of a space. Caller must be a member or the owner.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#memberView" + } + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json new file mode 100644 index 0000000..dea1884 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json @@ -0,0 +1,74 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.listRecords", + "defs": { + "main": { + "type": "query", + "description": "List records of a given collection within a space. Access is governed by the space's collection policy.", + "parameters": { + "type": "params", + "required": [ + "spaceUri", + "collection" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only return records authored by this DID." + }, + "cursor": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token. When supplied, replaces JWT auth for this read." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#recordView" + } + }, + "cursor": { + "type": "string" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json new file mode 100644 index 0000000..70a9e2e --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json @@ -0,0 +1,62 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.listSpaces", + "defs": { + "main": { + "type": "query", + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns. When scope='member', the optional 'owner' param narrows to spaces owned by that DID — useful for listing all channels in a specific community the caller can access.", + "parameters": { + "type": "params", + "properties": { + "scope": { + "type": "string", + "knownValues": [ + "member", + "owner" + ], + "default": "member" + }, + "type": { + "type": "string", + "format": "nsid" + }, + "owner": { + "type": "string", + "format": "did", + "description": "With scope=member, filter to spaces owned by this DID. Ignored when scope=owner." + }, + "cursor": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaces" + ], + "properties": { + "spaces": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.atmo.chat.space.defs#spaceView" + } + }, + "cursor": { + "type": "string" + } + } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json new file mode 100644 index 0000000..e453ed7 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json @@ -0,0 +1,68 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Write a record into a space. The author is always the JWT issuer. If rkey is omitted, a TID is generated.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "record" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "record": { + "type": "unknown" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "rkey", + "authorDid", + "createdAt" + ], + "properties": { + "rkey": { + "type": "string" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json new file mode 100644 index 0000000..48b61b4 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.removeMember", + "defs": { + "main": { + "type": "procedure", + "description": "Remove a member from a space. Owner only. Cannot remove the owner.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "did" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "InvalidRequest" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json new file mode 100644 index 0000000..ca8cc33 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json @@ -0,0 +1,50 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.uploadBlob", + "defs": { + "main": { + "type": "procedure", + "description": "Upload a blob into a space. Returns a standard atproto BlobRef that records in this space can then reference. The record will be rejected at putRecord time if it references a blob that was not uploaded to the same space.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "input": { + "encoding": "*/*" + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "blob" + ], + "properties": { + "blob": { + "type": "blob" + } + } + } + }, + "errors": [ + { + "name": "Forbidden" + }, + { + "name": "BlobTooLarge" + }, + { + "name": "InvalidMimeType" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json new file mode 100644 index 0000000..8e4f95f --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.space.whoami", + "defs": { + "main": { + "type": "query", + "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "isOwner", + "isMember" + ], + "properties": { + "isOwner": { + "type": "boolean" + }, + "isMember": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-pulled/README.md b/examples/sveltekit-group-chat/lexicons-pulled/README.md index 812df07..c6fc63b 100644 --- a/examples/sveltekit-group-chat/lexicons-pulled/README.md +++ b/examples/sveltekit-group-chat/lexicons-pulled/README.md @@ -2,4 +2,4 @@ this directory contains lexicon documents pulled from the following sources: -- atproto (nsids: app.bsky.actor.profile, xyz.statusphere.status) +- atproto (nsids: app.bsky.actor.profile, tools.atmo.chat.channel, tools.atmo.chat.message, tools.atmo.chat.server) diff --git a/examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json b/examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json deleted file mode 100644 index c5a314e..0000000 --- a/examples/sveltekit-group-chat/lexicons-pulled/xyz/statusphere/status.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "id": "xyz.statusphere.status", - "defs": { - "main": { - "key": "tid", - "type": "record", - "record": { - "type": "object", - "required": ["status", "createdAt"], - "properties": { - "status": { - "type": "string", - "maxLength": 32, - "minLength": 1, - "maxGraphemes": 1 - }, - "createdAt": { - "type": "string", - "format": "datetime" - } - } - } - } - }, - "$type": "com.atproto.lexicon.schema", - "lexicon": 1 -} diff --git a/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json b/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json new file mode 100644 index 0000000..adff23a --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.channel", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "record": { + "type": "object", + "required": ["communityDid", "name", "visibility", "createdAt"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "name": { "type": "string", "maxLength": 128 }, + "topic": { "type": "string", "maxLength": 512 }, + "visibility": { + "type": "string", + "knownValues": ["public", "private"] + }, + "createdAt": { "type": "string", "format": "datetime" } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json b/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json new file mode 100644 index 0000000..a0c8b8d --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json @@ -0,0 +1,19 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.message", + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["text", "createdAt"], + "properties": { + "text": { "type": "string", "maxLength": 4000 }, + "createdAt": { "type": "string", "format": "datetime" }, + "replyTo": { "type": "string", "description": "rkey of the parent message in the same channel" } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json b/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json new file mode 100644 index 0000000..582117c --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json @@ -0,0 +1,25 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.server", + "defs": { + "main": { + "type": "record", + "key": "literal:self", + "record": { + "type": "object", + "required": ["communityDid", "name", "createdAt"], + "properties": { + "communityDid": { "type": "string", "format": "did" }, + "name": { "type": "string", "maxLength": 128 }, + "description": { "type": "string", "maxLength": 2000 }, + "icon": { + "type": "blob", + "accept": ["image/png", "image/jpeg", "image/webp", "image/gif"], + "maxSize": 1000000 + }, + "createdAt": { "type": "string", "format": "datetime" } + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/package.json b/examples/sveltekit-group-chat/package.json index f45b5ab..abe6f0c 100644 --- a/examples/sveltekit-group-chat/package.json +++ b/examples/sveltekit-group-chat/package.json @@ -1,5 +1,5 @@ { - "name": "svelte-cloudflare-statusphere", + "name": "sveltekit-group-chat", "private": true, "version": "0.1.0", "type": "module", @@ -59,7 +59,7 @@ "license": "MIT", "dependencies": { "@atcute/jetstream": "^1.1.2", - "@atmo-dev/contrail": "^0.1.1", + "@atmo-dev/contrail": "workspace:*", "@foxui/core": "^0.9.1", "@foxui/social": "^0.8.10", "@foxui/time": "^0.8.5", diff --git a/examples/sveltekit-group-chat/scripts/append-scheduled.ts b/examples/sveltekit-group-chat/scripts/append-scheduled.ts index c9bd1cd..549821e 100644 --- a/examples/sveltekit-group-chat/scripts/append-scheduled.ts +++ b/examples/sveltekit-group-chat/scripts/append-scheduled.ts @@ -1,9 +1,9 @@ /** - * Post-build script: appends a `scheduled` handler to the SvelteKit worker output. - * - * SvelteKit's adapter-cloudflare doesn't support the `scheduled` export natively - * (see https://github.com/sveltejs/kit/issues/4841). This script patches the - * generated _worker.js to add one that self-calls the /api/cron endpoint. + * Post-build script: + * - appends a `scheduled` handler to the SvelteKit worker output + * (adapter-cloudflare doesn't support scheduled exports natively: + * https://github.com/sveltejs/kit/issues/4841) + * - re-exports the realtime Durable Object class so wrangler's class_name binding resolves. */ import { readFileSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; @@ -16,6 +16,9 @@ let code = readFileSync(workerPath, 'utf-8'); code += ` // --- Appended by scripts/append-scheduled.ts --- +import { RealtimePubSubDO as __RealtimePubSubDO } from "@atmo-dev/contrail"; +export { __RealtimePubSubDO as RealtimePubSubDO }; + worker_default.scheduled = async function (event, env, ctx) { const req = new Request('http://localhost/api/cron', { method: 'POST', @@ -26,4 +29,4 @@ worker_default.scheduled = async function (event, env, ctx) { `; writeFileSync(workerPath, code); -console.log('Appended scheduled handler to _worker.js'); +console.log('Appended scheduled handler + RealtimePubSubDO re-export to _worker.js'); diff --git a/examples/sveltekit-group-chat/scripts/generate.ts b/examples/sveltekit-group-chat/scripts/generate.ts index 5a1cb3a..7818daf 100644 --- a/examples/sveltekit-group-chat/scripts/generate.ts +++ b/examples/sveltekit-group-chat/scripts/generate.ts @@ -1,13 +1,36 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { config } from '../src/lib/contrail/config'; -import { generateLexicons } from '@atmo-dev/contrail/generate'; +import { writeFileSync } from 'fs'; +import { baseConfig } from '../src/lib/contrail/config'; +import { generateLexicons, extractXrpcMethods } from '@atmo-dev/contrail/generate'; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); -generateLexicons({ - config, +// Use a config that has spaces/community/realtime declared so the generator +// emits the full surface (space.*, community.*, realtime.*) alongside the +// per-collection endpoints. Values are stubs — only the shape matters for codegen. +const configForGen = { + ...baseConfig, + spaces: { type: 'tools.atmo.chat.space', serviceDid: 'did:web:localhost' }, + community: { masterKey: new Uint8Array(32), serviceDid: 'did:web:localhost' }, + realtime: { ticketSecret: new Uint8Array(32) } +}; + +const generated = generateLexicons({ + config: configForGen, rootDir: ROOT_DIR, outputDir: join(ROOT_DIR, 'lexicons-generated'), writeRuntimeFiles: true }); + +// Emit the sorted XRPC method list as a TS module. settings.ts imports it and +// feeds it into `scope.rpc({ lxm: [...], aud })` so the OAuth consent screen +// requests exactly the methods contrail exposes (no more, no less). +const methods = extractXrpcMethods(generated); +const out = `// Auto-generated by scripts/generate.ts. Do not edit by hand. +// Source of truth: the set of XRPC methods contrail emits for this namespace. + +export const xrpcMethods = ${JSON.stringify(methods, null, '\t')} as const; +`; +writeFileSync(join(ROOT_DIR, 'src/lib/atproto/generated-methods.ts'), out); +console.log(`wrote generated-methods.ts (${methods.length} methods)`); diff --git a/examples/sveltekit-group-chat/src/app.d.ts b/examples/sveltekit-group-chat/src/app.d.ts index 831d778..35167b9 100644 --- a/examples/sveltekit-group-chat/src/app.d.ts +++ b/examples/sveltekit-group-chat/src/app.d.ts @@ -3,6 +3,7 @@ import type { OAuthSession } from '@atcute/oauth-node-client'; import type { Client } from '@atcute/client'; import type { Did } from '@atcute/lexicons'; +import type { DurableObjectNamespace } from '@atmo-dev/contrail'; declare global { namespace App { @@ -24,6 +25,12 @@ declare global { PROFILE_CACHE?: KVNamespace; DB: D1Database; CRON_SECRET: string; + COMMUNITY_MASTER_KEY: string; + REALTIME_TICKET_SECRET: string; + SERVICE_DID: string; + DEV_AUTH?: string; + REALTIME: DurableObjectNamespace; + BLOBS?: R2Bucket; }; } } diff --git a/examples/sveltekit-group-chat/src/hooks.server.ts b/examples/sveltekit-group-chat/src/hooks.server.ts index 98b4fae..0c28a5d 100644 --- a/examples/sveltekit-group-chat/src/hooks.server.ts +++ b/examples/sveltekit-group-chat/src/hooks.server.ts @@ -1,4 +1,5 @@ import type { Handle } from '@sveltejs/kit'; +import './lexicon-types'; // register XRPCQueries/XRPCProcedures augmentations import { restoreSession } from '$lib/atproto/server/session'; export const handle: Handle = async ({ event, resolve }) => { diff --git a/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts b/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts new file mode 100644 index 0000000..e5d4872 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts @@ -0,0 +1,58 @@ +// Auto-generated by scripts/generate.ts. Do not edit by hand. +// Source of truth: the set of XRPC methods contrail emits for this namespace. + +export const xrpcMethods = [ + "tools.atmo.chat.channel.getRecord", + "tools.atmo.chat.channel.listRecords", + "tools.atmo.chat.community.adopt", + "tools.atmo.chat.community.delete", + "tools.atmo.chat.community.deleteRecord", + "tools.atmo.chat.community.getHealth", + "tools.atmo.chat.community.invite.create", + "tools.atmo.chat.community.invite.list", + "tools.atmo.chat.community.invite.redeem", + "tools.atmo.chat.community.invite.revoke", + "tools.atmo.chat.community.list", + "tools.atmo.chat.community.mint", + "tools.atmo.chat.community.putRecord", + "tools.atmo.chat.community.reauth", + "tools.atmo.chat.community.space.create", + "tools.atmo.chat.community.space.delete", + "tools.atmo.chat.community.space.deleteRecord", + "tools.atmo.chat.community.space.grant", + "tools.atmo.chat.community.space.listMembers", + "tools.atmo.chat.community.space.putRecord", + "tools.atmo.chat.community.space.resync", + "tools.atmo.chat.community.space.revoke", + "tools.atmo.chat.community.space.setAccessLevel", + "tools.atmo.chat.community.whoami", + "tools.atmo.chat.getCursor", + "tools.atmo.chat.getOverview", + "tools.atmo.chat.getProfile", + "tools.atmo.chat.message.getRecord", + "tools.atmo.chat.message.listRecords", + "tools.atmo.chat.notifyOfUpdate", + "tools.atmo.chat.realtime.subscribe", + "tools.atmo.chat.realtime.ticket", + "tools.atmo.chat.server.getRecord", + "tools.atmo.chat.server.listRecords", + "tools.atmo.chat.space.addMember", + "tools.atmo.chat.space.createSpace", + "tools.atmo.chat.space.deleteRecord", + "tools.atmo.chat.space.getBlob", + "tools.atmo.chat.space.getRecord", + "tools.atmo.chat.space.getSpace", + "tools.atmo.chat.space.invite.create", + "tools.atmo.chat.space.invite.list", + "tools.atmo.chat.space.invite.redeem", + "tools.atmo.chat.space.invite.revoke", + "tools.atmo.chat.space.leaveSpace", + "tools.atmo.chat.space.listBlobs", + "tools.atmo.chat.space.listMembers", + "tools.atmo.chat.space.listRecords", + "tools.atmo.chat.space.listSpaces", + "tools.atmo.chat.space.putRecord", + "tools.atmo.chat.space.removeMember", + "tools.atmo.chat.space.uploadBlob", + "tools.atmo.chat.space.whoami" +] as const; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts index a9db6fb..6d4c52c 100644 --- a/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts @@ -2,7 +2,7 @@ import { error } from '@sveltejs/kit'; import { command, getRequestEvent } from '$app/server'; import * as v from 'valibot'; import { collections } from '../settings'; -import { contrail, ensureInit } from '$lib/contrail'; +import { getContrail } from '$lib/contrail'; // Validate collection format and check against allowed list from settings const collectionSchema = v.pipe( @@ -35,10 +35,9 @@ export const putRecord = command( // Immediately index the new/updated record in contrail const { platform } = getRequestEvent(); - const db = platform?.env?.DB; - if (db) { - await ensureInit(db); - await contrail.notify(response.data.uri, db).catch(() => {}); + if (platform?.env) { + const contrail = await getContrail(platform.env); + await contrail.notify(response.data.uri, platform.env.DB).catch(() => {}); } return response.data; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts index 6cde19c..7f63f5d 100644 --- a/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts @@ -67,3 +67,40 @@ export function setSignedCookie( const signed = `${value}${SEPARATOR}${sig}`; cookies.set(name, signed, options); } + +/** Verify + read a signed cookie from a raw Request's Cookie header. + * Used from places that don't have a SvelteKit `Cookies` instance + * (e.g. contrail auth override). */ +export function getSignedCookieFromRequest(req: Request, name: string): string | null { + const header = req.headers.get('cookie'); + if (!header) return null; + const pairs = header.split(/;\s*/); + let raw: string | null = null; + for (const pair of pairs) { + const eq = pair.indexOf('='); + if (eq === -1) continue; + const key = pair.slice(0, eq); + if (key === name) { + raw = decodeURIComponent(pair.slice(eq + 1)); + break; + } + } + if (!raw) return null; + + const idx = raw.lastIndexOf(SEPARATOR); + if (idx === -1) return null; + const value = raw.slice(0, idx); + const sig = raw.slice(idx + 1); + + let expected: Uint8Array; + let got: Uint8Array; + try { + expected = hmacSha256(value); + got = fromBase64Url(sig); + } catch { + return null; + } + + if (got.length !== expected.length || !timingSafeEqual(got, expected)) return null; + return value; +} diff --git a/examples/sveltekit-group-chat/src/lib/atproto/settings.ts b/examples/sveltekit-group-chat/src/lib/atproto/settings.ts index b187107..1d81672 100644 --- a/examples/sveltekit-group-chat/src/lib/atproto/settings.ts +++ b/examples/sveltekit-group-chat/src/lib/atproto/settings.ts @@ -1,16 +1,32 @@ import { dev } from '$app/environment'; import { scope } from '@atcute/oauth-node-client'; +import { xrpcMethods } from './generated-methods'; -// writable collections -export const collections = ['xyz.statusphere.status', 'social.atmo.test.blob'] as const; +// Writable collections on the USER's PDS. This app writes nothing to the user's +// PDS — every record lives in contrail spaces, authored by the user DID via +// space.putRecord or by the community DID via community.space.putRecord. +// Keep the list empty; repo scope is omitted below. +export const collections = [] as const; export type AllowedCollection = (typeof collections)[number]; -// OAuth scope — add scope.blob(), scope.rpc(), etc. as needed +// OAuth scope. +// +// We ask for: +// - `atproto` — baseline identity scope. +// - `rpc` for every XRPC method contrail emits in this namespace. The list +// is auto-generated by `pnpm generate` (source: contrail's permissionSet), +// so adding/removing collections or toggling modules automatically updates +// the consent prompt. +// - `aud: '*'` — we don't couple the scope to a specific service DID so one +// OAuth client metadata works in dev and prod. The PDS still only mints +// JWTs for the exact `aud` the app asks for at getServiceAuth time. export const scopes = [ 'atproto', - scope.repo({ collection: [...collections] }), - scope.blob({ accept: ['image/*'] }) + scope.rpc({ + aud: '*', + lxm: [...xrpcMethods] as `${string}.${string}.${string}`[] + }) ]; // set to false to disable signup diff --git a/examples/sveltekit-group-chat/src/lib/contrail/config.ts b/examples/sveltekit-group-chat/src/lib/contrail/config.ts index b3aa09a..a01c02f 100644 --- a/examples/sveltekit-group-chat/src/lib/contrail/config.ts +++ b/examples/sveltekit-group-chat/src/lib/contrail/config.ts @@ -1,12 +1,29 @@ import type { ContrailConfig } from '@atmo-dev/contrail'; -export const config: ContrailConfig = { - namespace: 'statusphere.app', +/** Env-independent contrail config. Spaces/community/realtime are enabled per-request + * from `getContrail(env)` in `./index.ts`, since those modules need bindings or secrets + * that live on `platform.env`. */ +export const namespace = 'tools.atmo.chat'; + +export const baseConfig: ContrailConfig = { + namespace, collections: { - status: { - collection: 'xyz.statusphere.status', + server: { + collection: 'tools.atmo.chat.server', + queryable: { + communityDid: {} + } + }, + channel: { + collection: 'tools.atmo.chat.channel', + queryable: { + communityDid: {}, + visibility: {} + } + }, + message: { + collection: 'tools.atmo.chat.message', queryable: { - status: {}, createdAt: { type: 'range' } } } diff --git a/examples/sveltekit-group-chat/src/lib/contrail/index.ts b/examples/sveltekit-group-chat/src/lib/contrail/index.ts index aa8c784..0c0b382 100644 --- a/examples/sveltekit-group-chat/src/lib/contrail/index.ts +++ b/examples/sveltekit-group-chat/src/lib/contrail/index.ts @@ -1,31 +1,128 @@ -import { Contrail } from '@atmo-dev/contrail'; +import { + Contrail, + DurableObjectPubSub, + InMemoryPubSub, + MemoryBlobAdapter, + R2BlobAdapter, + type BlobAdapter, + type ContrailConfig, + type DurableObjectNamespace, + type PubSub, + type R2BucketLike +} from '@atmo-dev/contrail'; import { createHandler } from '@atmo-dev/contrail/server'; import { Client } from '@atcute/client'; -import { config } from './config'; +import { baseConfig } from './config'; +import { getSignedCookieFromRequest } from '$lib/atproto/server/signed-cookie'; -export const contrail = new Contrail(config); +type Env = App.Platform['env']; -let initialized = false; +interface Bundle { + contrail: Contrail; + handle: (req: Request, db?: unknown) => unknown; + ready: Promise; +} + +let cached: { env: Env; bundle: Bundle } | null = null; + +function build(env: Env): Bundle { + const devAuth = env.DEV_AUTH === '1'; + + // In dev the DO class isn't wired (vite runs the worker directly, skipping + // the post-build re-export), so fall back to InMemoryPubSub. Single-isolate + // is fine for one-user dev; production uses the DO so publishes fan out + // across isolates. + const pubsub: PubSub = devAuth + ? new InMemoryPubSub() + : new DurableObjectPubSub(env.REALTIME as DurableObjectNamespace); + + // Blob storage: in dev, an in-isolate memory adapter (bytes reset on + // restart). In prod, bind an R2 bucket as `env.BLOBS` and it'll be used. + const blobAdapter: BlobAdapter = env.BLOBS + ? new R2BlobAdapter(env.BLOBS as R2BucketLike) + : new MemoryBlobAdapter(); + + const config: ContrailConfig = { + ...baseConfig, + spaces: { + type: 'tools.atmo.chat.space', + serviceDid: env.SERVICE_DID, + blobs: { + adapter: blobAdapter, + maxSize: 2 * 1024 * 1024, + accept: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] + }, + // Dev-only bypass: trust the HMAC-signed `did` cookie the OAuth flow + // sets, in lieu of a service-auth JWT. bsky.social rejects + // `getServiceAuth` for loopback clients, so without this the demo + // would need a tunnel. NEVER set DEV_AUTH=1 in prod. + authOverride: devAuth + ? (req: Request) => { + // Accept either the HMAC-signed session cookie (from the + // browser via /xrpc/...) or an X-Dev-Did header (from the + // server-side remote helpers, which construct synthetic + // requests and already trust the session themselves). + const headerDid = req.headers.get('x-dev-did'); + const did = headerDid ?? getSignedCookieFromRequest(req, 'did'); + if (!did) return null; + return { + issuer: did, + audience: env.SERVICE_DID + }; + } + : undefined + }, + community: { + masterKey: env.COMMUNITY_MASTER_KEY, + serviceDid: env.SERVICE_DID + }, + realtime: { + ticketSecret: env.REALTIME_TICKET_SECRET, + pubsub + } + }; -export async function ensureInit(db: D1Database) { - if (!initialized) { - await contrail.init(db); - initialized = true; - } + const contrail = new Contrail(config); + const handle = createHandler(contrail); + const ready = contrail.init(env.DB); + + return { contrail, handle: handle as Bundle['handle'], ready }; +} + +function getBundle(env: Env): Bundle { + if (cached && cached.env === env) return cached.bundle; + const bundle = build(env); + cached = { env, bundle }; + return bundle; } -const handle = createHandler(contrail); +/** Get the shared contrail instance, bound to this worker's env. */ +export async function getContrail(env: Env): Promise { + const b = getBundle(env); + await b.ready; + return b.contrail; +} + +/** Dispatch a Request through contrail's XRPC handler. */ +export async function dispatch(req: Request, env: Env): Promise { + const b = getBundle(env); + await b.ready; + return (await b.handle(req, env.DB)) as Response; +} -/** - * Server-side: fully typed @atcute/client that routes through contrail in-process. - * No HTTP roundtrip — calls createHandler directly. - */ -export function getServerClient(db: D1Database) { +/** Server-side typed @atcute client that routes through contrail in-process. + * When `asDid` is set, every request carries an Authorization header identifying + * the caller — this is used only for server-originated bootstrap calls (e.g. the + * community mint flow) where we already have the user's DID from the OAuth session + * but want to bypass the atproto PDS round-trip. For normal user actions, route + * through the user's OAuth client instead. */ +export function getServerClient(env: Env): Client { return new Client({ handler: async (pathname, init) => { - await ensureInit(db); + const b = getBundle(env); + await b.ready; const url = new URL(pathname, 'http://localhost'); - return handle(new Request(url, init), db) as Promise; + return (await b.handle(new Request(url, init), env.DB)) as Response; } }); } diff --git a/examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts b/examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts new file mode 100644 index 0000000..9a13b7f --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts @@ -0,0 +1,65 @@ +/** Thin SvelteKit remote-command wrappers around contrail's community.invite.* + * XRPCs. Contrail owns the token storage, atomic redemption, and access-level + * authorization checks. */ + +import { error } from '@sveltejs/kit'; +import { command, getRequestEvent } from '$app/server'; +import * as v from 'valibot'; +import type { Client } from '@atcute/client'; +import { + createCommunityInvite, + redeemCommunityInvite, + revokeCommunityInvite +} from './server'; + +function requireAuthed() { + const event = getRequestEvent(); + const env = event.platform?.env; + const client = event.locals.client; + const did = event.locals.did; + if (!env) error(500, 'platform env unavailable'); + if (!client || !did) error(401, 'Not authenticated'); + return { env, client: client as Client, did: did as string }; +} + +const AccessLevel = v.picklist(['member', 'manager', 'admin', 'owner']); + +const CreateInviteInput = v.object({ + spaceUri: v.pipe(v.string(), v.minLength(1)), + accessLevel: v.optional(AccessLevel), + expiresInMinutes: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(60 * 24 * 90))), + maxUses: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(10000))), + note: v.optional(v.pipe(v.string(), v.maxLength(500))) +}); + +export const createInvite = command(CreateInviteInput, async (input) => { + const ctx = requireAuthed(); + const expiresAt = + input.expiresInMinutes != null ? Date.now() + input.expiresInMinutes * 60_000 : undefined; + const res = await createCommunityInvite(ctx, { + spaceUri: input.spaceUri, + accessLevel: input.accessLevel ?? 'member', + expiresAt, + maxUses: input.maxUses, + note: input.note + }); + return { token: res.token, tokenHash: res.tokenHash }; +}); + +const RevokeInviteInput = v.object({ + tokenHash: v.pipe(v.string(), v.minLength(1)) +}); + +export const revokeInvite = command(RevokeInviteInput, async (input) => { + const ctx = requireAuthed(); + return revokeCommunityInvite(ctx, { tokenHash: input.tokenHash }); +}); + +const RedeemInviteInput = v.object({ + token: v.pipe(v.string(), v.minLength(8)) +}); + +export const redeemInvite = command(RedeemInviteInput, async (input) => { + const ctx = requireAuthed(); + return redeemCommunityInvite(ctx, { token: input.token }); +}); diff --git a/examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts new file mode 100644 index 0000000..7091cd7 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts @@ -0,0 +1,70 @@ +/** Browser-side profile cache. DIDs → handle / displayName / avatar. + * Lazy: first read triggers a `.getProfile` fetch in the background; + * the reactive store updates when the fetch resolves, so templates reading + * the display string rerender automatically. + * + * Kept dead simple — no eviction, no refresh. A single user session touches + * at most a handful of DIDs, so a plain record is fine. */ + +import { untrack } from 'svelte'; + +export interface ProfileEntry { + did: string; + handle?: string; + displayName?: string; + avatar?: string; + /** true if we tried and failed — stops us from retrying forever. */ + failed?: boolean; +} + +export const profiles = $state>({}); +const inFlight = new Set(); + +/** Request a profile fetch if we don't have it. Safe to call from templates. */ +export function ensureProfile(did: string): void { + if (!did) return; + if (untrack(() => profiles[did] || inFlight.has(did))) return; + inFlight.add(did); + void (async () => { + try { + const res = await fetch( + `/xrpc/tools.atmo.chat.getProfile?actor=${encodeURIComponent(did)}` + ); + if (!res.ok) throw new Error(`getProfile ${res.status}`); + const data = (await res.json()) as { + profiles?: Array<{ + did: string; + handle?: string | null; + record?: { displayName?: string; avatar?: string }; + }>; + }; + const entry = data.profiles?.[0]; + profiles[did] = { + did, + handle: entry?.handle ?? undefined, + displayName: entry?.record?.displayName, + avatar: entry?.record?.avatar + }; + } catch { + profiles[did] = { did, failed: true }; + } finally { + inFlight.delete(did); + } + })(); +} + +/** Display name for a DID. Returns handle if known, else a truncated DID. + * Calling this from a template also kicks off a background fetch. */ +export function displayName(did: string): string { + ensureProfile(did); + const p = profiles[did]; + if (p?.displayName) return p.displayName; + if (p?.handle) return p.handle; + return did.length > 14 ? `${did.slice(0, 14)}…` : did; +} + +/** Handle for a DID, if known. Returns null when we don't have it yet. */ +export function handleOf(did: string): string | null { + ensureProfile(did); + return profiles[did]?.handle ?? null; +} diff --git a/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts new file mode 100644 index 0000000..3b03d1f --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts @@ -0,0 +1,216 @@ +/** Browser-side realtime: single EventSource against + * `.realtime.subscribe?ticket=…`, keyed on `community:`. + * - auto-renews the ticket before expiry + * - forwards `record.created` into `channelMessages` + * - bumps `unread` counts for channels that aren't open + * - invalidates the current page (channel list) when new channel records arrive + */ + +import { untrack } from 'svelte'; +import { invalidateAll } from '$app/navigation'; +import { bumpUnread } from './unread.svelte'; + +async function fetchTicket( + topic: string +): Promise<{ ticket: string; topics: string[]; expiresAt: number }> { + const res = await fetch('/api/ticket', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ topic }) + }); + if (!res.ok) throw new Error(`ticket mint failed: ${res.status}`); + return res.json() as Promise<{ ticket: string; topics: string[]; expiresAt: number }>; +} + +export interface ChatMessage { + rkey: string; + authorDid: string; + text: string; + createdAt: string; + replyTo?: string; +} + +// ---- message store per-space --------------------------------------------- + +const byChannel = $state>({}); + +export const channelMessages = { + /** Reactive — safe to read in templates / $derived. */ + get(spaceUri: string): ChatMessage[] { + return byChannel[spaceUri] ?? []; + }, + /** Seed the store from a server-rendered initial message list. Merges with + * any realtime events that arrived before hydration. Fully untracked so + * calling this from a `$effect` doesn't re-enter the effect. */ + seed(spaceUri: string, messages: ChatMessage[]) { + untrack(() => { + const existing = byChannel[spaceUri]; + if (!existing) { + byChannel[spaceUri] = [...messages]; + return; + } + const seen = new Set(existing.map((m) => m.rkey)); + const merged = [...existing]; + for (const m of messages) { + if (!seen.has(m.rkey)) { + seen.add(m.rkey); + merged.push(m); + } + } + merged.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); + byChannel[spaceUri] = merged; + }); + }, + append(spaceUri: string, m: ChatMessage) { + untrack(() => { + const arr = byChannel[spaceUri] ?? []; + if (arr.some((x) => x.rkey === m.rkey)) return; + byChannel[spaceUri] = [...arr, m].sort((a, b) => + a.createdAt < b.createdAt ? -1 : 1 + ); + }); + }, + remove(spaceUri: string, rkey: string) { + untrack(() => { + const arr = byChannel[spaceUri]; + if (!arr) return; + byChannel[spaceUri] = arr.filter((m) => m.rkey !== rkey); + }); + } +}; + +// ---- connection ---------------------------------------------------------- + +interface Connection { + close(): void; +} + +let current: { key: string; conn: Connection } | null = null; + +export function connectCommunityRealtime(communityDid: string): () => void { + const key = `community:${communityDid}`; + // Re-use if already connected. + if (current?.key === key) return current.conn.close; + + current?.conn.close(); + + let closed = false; + let es: EventSource | null = null; + let renewTimer: ReturnType | null = null; + + const handleEvent = (rawData: string, kind: string) => { + let ev: { + topic: string; + kind: string; + payload: Record; + ts: number; + }; + try { + ev = JSON.parse(rawData); + } catch { + return; + } + if (kind === 'record.created') { + const p = ev.payload as { + spaceUri: string; + collection: string; + authorDid: string; + rkey: string; + record: Record; + createdAt: number | string; + }; + if (p.collection === 'tools.atmo.chat.message') { + const rec = p.record as { text?: string; createdAt?: string; replyTo?: string }; + if (rec.text && rec.createdAt) { + channelMessages.append(p.spaceUri, { + rkey: p.rkey, + authorDid: p.authorDid, + text: rec.text, + createdAt: rec.createdAt, + replyTo: rec.replyTo + }); + // Bump unread if we're not currently on this channel. + if (!isCurrentChannel(p.spaceUri)) { + bumpUnread(p.spaceUri, rec.createdAt); + } + } + } else if (p.collection === 'tools.atmo.chat.channel') { + void invalidateAll(); + } + } else if (kind === 'record.deleted') { + const p = ev.payload as { spaceUri: string; collection: string; rkey: string }; + if (p.collection === 'tools.atmo.chat.message') { + channelMessages.remove(p.spaceUri, p.rkey); + } else if (p.collection === 'tools.atmo.chat.channel') { + void invalidateAll(); + } + } else if (kind === 'member.added' || kind === 'member.removed') { + void invalidateAll(); + } + }; + + async function open() { + if (closed) return; + let ticketRes: { ticket: string; topics: string[]; expiresAt: number }; + try { + ticketRes = await fetchTicket(key); + } catch (err) { + console.warn('realtime ticket mint failed', err); + // backoff + retry + renewTimer = setTimeout(open, 5000); + return; + } + + const url = `/xrpc/tools.atmo.chat.realtime.subscribe?ticket=${encodeURIComponent( + ticketRes.ticket + )}`; + es = new EventSource(url); + + es.addEventListener('record.created', (e) => handleEvent((e as MessageEvent).data, 'record.created')); + es.addEventListener('record.deleted', (e) => handleEvent((e as MessageEvent).data, 'record.deleted')); + es.addEventListener('member.added', (e) => handleEvent((e as MessageEvent).data, 'member.added')); + es.addEventListener('member.removed', (e) => handleEvent((e as MessageEvent).data, 'member.removed')); + + es.addEventListener('error', () => { + // Browser retries automatically; only act if the stream is closed. + if (es?.readyState === EventSource.CLOSED && !closed) { + renewTimer = setTimeout(open, 3000); + } + }); + + // Renew ~20s before expiry. + const ttl = Math.max(ticketRes.expiresAt - Date.now() - 20_000, 30_000); + renewTimer = setTimeout(() => { + es?.close(); + es = null; + void open(); + }, ttl); + } + + void open(); + + const conn: Connection = { + close() { + closed = true; + if (renewTimer) clearTimeout(renewTimer); + es?.close(); + if (current?.key === key) current = null; + } + }; + + current = { key, conn }; + return () => conn.close(); +} + +// ---- helpers ------------------------------------------------------------- + +function isCurrentChannel(spaceUri: string): boolean { + if (typeof window === 'undefined') return false; + return currentChannelSpaceUri === spaceUri; +} + +let currentChannelSpaceUri: string | null = null; + +export function setCurrentChannel(spaceUri: string | null) { + currentChannelSpaceUri = spaceUri; +} diff --git a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts new file mode 100644 index 0000000..f61a27c --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts @@ -0,0 +1,218 @@ +/** SvelteKit remote functions the browser can invoke directly. They run on + * the server, so they have access to `locals.client` (OAuth session) and + * `platform.env` (D1, DO, secrets). */ + +import { error } from '@sveltejs/kit'; +import { command, getRequestEvent } from '$app/server'; +import * as v from 'valibot'; +import type { Client } from '@atcute/client'; +import { + createSpace, + communityPutRecord, + grantAccess, + mintCommunity, + spacePutRecord, + revokeAccess, + setAccessLevel, + uploadBlob +} from './server'; +import { buildMembersUri } from './uri'; + +function requireAuthed() { + const event = getRequestEvent(); + const env = event.platform?.env; + const client = event.locals.client; + const did = event.locals.did; + if (!env) error(500, 'platform env unavailable'); + if (!client || !did) error(401, 'Not authenticated'); + return { env, client: client as Client, did: did as string }; +} + +// --------------------------------------------------------------------------- +// createCommunity — mint + bootstrap members space + write server record. +// Returns { communityDid, recoveryKey }. The UI must show recoveryKey once. +// --------------------------------------------------------------------------- + +const CreateCommunityInput = v.object({ + name: v.pipe(v.string(), v.minLength(1), v.maxLength(128)), + description: v.optional(v.pipe(v.string(), v.maxLength(2000))), + handle: v.optional(v.pipe(v.string(), v.maxLength(256))), + /** Optional icon blob ref. The bytes must already have been uploaded to + * the community's members space via /api/blob-upload before this call — + * but the members space doesn't exist yet at mint time, so the upload + * flow is: createCommunity first, then separately upload+update. For + * convenience, this endpoint also accepts an icon that was uploaded + * *during* the same request — it stitches it in by reuploading into the + * members space after creation. */ + icon: v.optional( + v.object({ + /** Raw bytes as a base64 string — small enough to inline. */ + bytes: v.string(), + mimeType: v.string() + }) + ) +}); + +export const createCommunity = command(CreateCommunityInput, async (input) => { + const ctx = requireAuthed(); + + const mint = await mintCommunity(ctx, input.handle ? { handle: input.handle } : {}); + const communityDid = mint.communityDid; + + const space = await createSpace(ctx, { communityDid, key: 'members' }); + const membersUri = space.space.uri; + + // If an icon was provided, upload it to the members space first, then + // include its blob ref on the server record. + let iconRef: import('./server').BlobRef | undefined; + if (input.icon) { + const bytes = Uint8Array.from(atob(input.icon.bytes), (c) => c.charCodeAt(0)); + iconRef = await uploadBlob(ctx, { + spaceUri: membersUri, + mimeType: input.icon.mimeType, + bytes + }); + } + + await communityPutRecord(ctx, { + spaceUri: membersUri, + collection: 'tools.atmo.chat.server', + rkey: 'self', + record: { + communityDid, + name: input.name, + description: input.description, + ...(iconRef ? { icon: iconRef } : {}), + createdAt: new Date().toISOString() + } + }); + + return { + communityDid, + membersUri, + recoveryKey: mint.recoveryKey + }; +}); + +// --------------------------------------------------------------------------- +// createChannel — new channel space + optional members-delegation (public) or +// direct DID grants (private) + channel record. +// --------------------------------------------------------------------------- + +const CreateChannelInput = v.object({ + communityDid: v.pipe(v.string(), v.minLength(1)), + name: v.pipe(v.string(), v.minLength(1), v.maxLength(128)), + topic: v.optional(v.pipe(v.string(), v.maxLength(512))), + visibility: v.picklist(['public', 'private']), + /** For private channels: DIDs to grant direct member access. */ + memberDids: v.optional(v.array(v.pipe(v.string(), v.minLength(1)))) +}); + +export const createChannel = command(CreateChannelInput, async (input) => { + const ctx = requireAuthed(); + + const created = await createSpace(ctx, { communityDid: input.communityDid }); + const channelUri = created.space.uri; + const channelKey = created.space.key; + + if (input.visibility === 'public') { + await grantAccess(ctx, { + spaceUri: channelUri, + subject: { spaceUri: buildMembersUri(input.communityDid) }, + accessLevel: 'member' + }); + } else { + for (const did of input.memberDids ?? []) { + // Ensure the user is in `members` too so they get the server header. + // Ignore duplicates; grant is upsert. + await grantAccess(ctx, { + spaceUri: buildMembersUri(input.communityDid), + subject: { did }, + accessLevel: 'member' + }).catch(() => {}); + await grantAccess(ctx, { + spaceUri: channelUri, + subject: { did }, + accessLevel: 'member' + }); + } + } + + await communityPutRecord(ctx, { + spaceUri: channelUri, + collection: 'tools.atmo.chat.channel', + rkey: 'self', + record: { + communityDid: input.communityDid, + name: input.name, + topic: input.topic, + visibility: input.visibility, + createdAt: new Date().toISOString() + } + }); + + return { channelUri, channelKey }; +}); + +// --------------------------------------------------------------------------- +// postMessage — append a chat message to a channel. +// --------------------------------------------------------------------------- + +const PostMessageInput = v.object({ + spaceUri: v.pipe(v.string(), v.minLength(1)), + text: v.pipe(v.string(), v.minLength(1), v.maxLength(4000)), + replyTo: v.optional(v.pipe(v.string(), v.minLength(1))) +}); + +export const postMessage = command(PostMessageInput, async (input) => { + const ctx = requireAuthed(); + const res = await spacePutRecord(ctx, { + spaceUri: input.spaceUri, + collection: 'tools.atmo.chat.message', + record: { + text: input.text, + createdAt: new Date().toISOString(), + ...(input.replyTo ? { replyTo: input.replyTo } : {}) + } + }); + return res; +}); + +// --------------------------------------------------------------------------- +// grantMember / revokeMember / setMemberLevel — used in settings UI. +// --------------------------------------------------------------------------- + +const GrantMemberInput = v.object({ + spaceUri: v.pipe(v.string(), v.minLength(1)), + did: v.pipe(v.string(), v.minLength(1)), + accessLevel: v.picklist(['member', 'manager', 'admin', 'owner']) +}); + +export const grantMember = command(GrantMemberInput, async (input) => { + const ctx = requireAuthed(); + return grantAccess(ctx, { + spaceUri: input.spaceUri, + subject: { did: input.did }, + accessLevel: input.accessLevel + }); +}); + +const RevokeMemberInput = v.object({ + spaceUri: v.pipe(v.string(), v.minLength(1)), + did: v.pipe(v.string(), v.minLength(1)) +}); + +export const revokeMember = command(RevokeMemberInput, async (input) => { + const ctx = requireAuthed(); + return revokeAccess(ctx, { spaceUri: input.spaceUri, subject: { did: input.did } }); +}); + +export const setMemberLevel = command(GrantMemberInput, async (input) => { + const ctx = requireAuthed(); + return setAccessLevel(ctx, { + spaceUri: input.spaceUri, + subject: { did: input.did }, + accessLevel: input.accessLevel + }); +}); + diff --git a/examples/sveltekit-group-chat/src/lib/rooms/server.ts b/examples/sveltekit-group-chat/src/lib/rooms/server.ts new file mode 100644 index 0000000..a8b429d --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/server.ts @@ -0,0 +1,340 @@ +/** Server-side helpers for calling contrail's authenticated XRPCs. + * + * Contrail's community/space/realtime endpoints authenticate via atproto + * service-auth JWTs. The caller's OAuth session on their PDS can mint those + * JWTs via `com.atproto.server.getServiceAuth`. This module wraps that dance: + * mint → dispatch → return the typed body. + */ + +import type { Client } from '@atcute/client'; +import { dispatch } from '$lib/contrail'; + +type Env = App.Platform['env']; + +interface AuthedCallContext { + env: Env; + /** Caller's OAuth-authenticated atproto client (talks to their PDS). */ + client: Client; + /** Caller's DID. */ + did: string; +} + +/** Authenticated fetch for page loaders — mints a service-auth JWT + * (or takes the dev bypass) and dispatches to contrail in-process. + * Returns the parsed JSON body, or throws with status + payload. */ +export async function authedFetch( + ctx: AuthedCallContext, + method: string, + opts: { query?: Record; body?: unknown } = {} +): Promise { + return callContrail(ctx, method, opts); +} + +async function mintServiceAuth( + ctx: AuthedCallContext, + lxm: string, + expSeconds = 60 +): Promise { + // Dev bypass: the contrail authOverride trusts our signed session cookie + // instead of a real JWT, so we don't need to hit the PDS. Returns a sentinel + // the override ignores (any string is fine — it's never verified). + if (ctx.env.DEV_AUTH === '1') { + return 'dev'; + } + const res = await ctx.client.get('com.atproto.server.getServiceAuth', { + params: { + aud: ctx.env.SERVICE_DID as `did:${string}:${string}`, + lxm: lxm as `${string}.${string}.${string}`, + exp: Math.floor(Date.now() / 1000) + expSeconds + } + }); + if (!res.ok) { + throw new Error(`getServiceAuth failed: ${JSON.stringify(res.data)}`); + } + const data = res.data as { token: string }; + return data.token; +} + +async function callContrail( + ctx: AuthedCallContext, + method: string, + opts: { body?: unknown; query?: Record; httpMethod?: 'GET' | 'POST' } +): Promise { + const jwt = await mintServiceAuth(ctx, method); + const url = new URL(`http://localhost/xrpc/${method}`); + for (const [k, v] of Object.entries(opts.query ?? {})) { + url.searchParams.set(k, v); + } + const httpMethod = opts.httpMethod ?? (opts.body === undefined ? 'GET' : 'POST'); + const devAuth = ctx.env.DEV_AUTH === '1'; + const headers: Record = { + Authorization: `Bearer ${jwt}`, + ...(opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}) + }; + if (devAuth) headers['X-Dev-Did'] = ctx.did; + const req = new Request(url, { + method: httpMethod, + headers, + body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined + }); + const res = await dispatch(req, ctx.env); + const text = await res.text(); + const json = text ? JSON.parse(text) : {}; + if (!res.ok) { + const err = new Error(`${method} failed (${res.status}): ${text}`); + (err as Error & { status: number; payload: unknown }).status = res.status; + (err as Error & { status: number; payload: unknown }).payload = json; + throw err; + } + return json as T; +} + +// --- community lifecycle ---------------------------------------------------- + +export async function mintCommunity( + ctx: AuthedCallContext, + input: { handle?: string } = {} +): Promise<{ communityDid: string; recoveryKey: unknown }> { + return callContrail(ctx, 'tools.atmo.chat.community.mint', { body: input }); +} + +// --- space (channel / role) lifecycle -------------------------------------- + +export async function createSpace( + ctx: AuthedCallContext, + input: { communityDid: string; key?: string } +): Promise<{ space: { uri: string; key: string; ownerDid: string } }> { + return callContrail(ctx, 'tools.atmo.chat.community.space.create', { body: input }); +} + +export async function grantAccess( + ctx: AuthedCallContext, + input: { + spaceUri: string; + subject: { did?: string; spaceUri?: string }; + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; + } +): Promise<{ ok: true }> { + return callContrail(ctx, 'tools.atmo.chat.community.space.grant', { body: input }); +} + +export async function revokeAccess( + ctx: AuthedCallContext, + input: { spaceUri: string; subject: { did?: string; spaceUri?: string } } +): Promise<{ ok: true }> { + return callContrail(ctx, 'tools.atmo.chat.community.space.revoke', { body: input }); +} + +export async function setAccessLevel( + ctx: AuthedCallContext, + input: { + spaceUri: string; + subject: { did?: string; spaceUri?: string }; + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; + } +): Promise<{ ok: true }> { + return callContrail(ctx, 'tools.atmo.chat.community.space.setAccessLevel', { body: input }); +} + +// --- community records (authored by community DID) ------------------------- + +export async function communityPutRecord( + ctx: AuthedCallContext, + input: { + spaceUri: string; + collection: string; + rkey?: string; + record: Record; + } +): Promise<{ rkey: string; authorDid: string; createdAt: number }> { + return callContrail(ctx, 'tools.atmo.chat.community.space.putRecord', { body: input }); +} + +// --- user records in a space ----------------------------------------------- + +export async function spacePutRecord( + ctx: AuthedCallContext, + input: { + spaceUri: string; + collection: string; + rkey?: string; + record: Record; + } +): Promise<{ rkey: string; authorDid: string; createdAt: number }> { + return callContrail(ctx, 'tools.atmo.chat.space.putRecord', { body: input }); +} + +export async function spaceListRecords( + ctx: AuthedCallContext, + query: { spaceUri: string; collection: string; limit?: number; cursor?: string } +): Promise<{ + records: Array<{ + spaceUri: string; + collection: string; + authorDid: string; + rkey: string; + cid: string | null; + record: Record; + createdAt: number; + }>; + cursor?: string; +}> { + const q: Record = { + spaceUri: query.spaceUri, + collection: query.collection + }; + if (query.limit) q.limit = String(query.limit); + if (query.cursor) q.cursor = query.cursor; + return callContrail(ctx, 'tools.atmo.chat.space.listRecords', { query: q }); +} + +// --- members --------------------------------------------------------------- + +export async function listSpaceMembers( + ctx: AuthedCallContext, + query: { spaceUri: string; flatten?: boolean } +): Promise< + | { members: Array<{ did: string; addedAt: number }> } + | { + rows: Array<{ + subject: { did?: string; spaceUri?: string }; + accessLevel: string; + grantedBy: string; + grantedAt: number; + }>; + } +> { + const q: Record = { spaceUri: query.spaceUri }; + if (query.flatten) q.flatten = 'true'; + return callContrail(ctx, 'tools.atmo.chat.community.space.listMembers', { query: q }); +} + +export async function whoami( + ctx: AuthedCallContext, + query: { spaceUri: string } +): Promise<{ spaceUri: string; accessLevel: string | null }> { + return callContrail(ctx, 'tools.atmo.chat.community.whoami', { query }); +} + +// --- realtime -------------------------------------------------------------- + +export async function getRealtimeTicket( + ctx: AuthedCallContext, + input: { topic: string } +): Promise<{ ticket: string; topics: string[]; expiresAt: number }> { + return callContrail(ctx, 'tools.atmo.chat.realtime.ticket', { body: input }); +} + +// --- invites --------------------------------------------------------------- + +export interface InviteView { + tokenHash: string; + spaceUri: string; + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; + createdBy: string; + createdAt: number; + expiresAt: number | null; + maxUses: number | null; + usedCount: number; + revokedAt: number | null; + note: string | null; +} + +export async function createCommunityInvite( + ctx: AuthedCallContext, + input: { + spaceUri: string; + accessLevel: 'member' | 'manager' | 'admin' | 'owner'; + expiresAt?: number; + maxUses?: number; + note?: string; + } +): Promise<{ + token: string; + tokenHash: string; + spaceUri: string; + accessLevel: string; + expiresAt: number | null; + maxUses: number | null; + createdAt: number; +}> { + return callContrail(ctx, 'tools.atmo.chat.community.invite.create', { body: input }); +} + +export async function listCommunityInvites( + ctx: AuthedCallContext, + query: { spaceUri: string; includeRevoked?: boolean } +): Promise<{ invites: InviteView[] }> { + const q: Record = { spaceUri: query.spaceUri }; + if (query.includeRevoked) q.includeRevoked = 'true'; + return callContrail(ctx, 'tools.atmo.chat.community.invite.list', { query: q }); +} + +export async function revokeCommunityInvite( + ctx: AuthedCallContext, + input: { tokenHash: string } +): Promise<{ ok: true }> { + return callContrail(ctx, 'tools.atmo.chat.community.invite.revoke', { body: input }); +} + +export async function redeemCommunityInvite( + ctx: AuthedCallContext, + input: { token: string } +): Promise<{ spaceUri: string; accessLevel: string; communityDid: string }> { + return callContrail(ctx, 'tools.atmo.chat.community.invite.redeem', { body: input }); +} + +// --- blobs ----------------------------------------------------------------- + +export interface BlobRef { + $type: 'blob'; + ref: { $link: string }; + mimeType: string; + size: number; +} + +/** Upload raw bytes to a space's blob store. Unlike the other helpers this one + * uses a binary body (the raw bytes) rather than JSON, so we build the + * Request ourselves. Returns the blob ref for use in a record field. */ +export async function uploadBlob( + ctx: AuthedCallContext, + input: { spaceUri: string; mimeType: string; bytes: Uint8Array } +): Promise { + const method = 'tools.atmo.chat.space.uploadBlob'; + const jwt = ctx.env.DEV_AUTH === '1' ? 'dev' : await mintServiceAuthForUpload(ctx, method); + const url = new URL(`http://localhost/xrpc/${method}`); + url.searchParams.set('spaceUri', input.spaceUri); + const headers: Record = { + Authorization: `Bearer ${jwt}`, + 'Content-Type': input.mimeType, + 'Content-Length': String(input.bytes.byteLength) + }; + if (ctx.env.DEV_AUTH === '1') headers['X-Dev-Did'] = ctx.did; + + const req = new Request(url, { + method: 'POST', + headers, + body: new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }) + }); + const res = await dispatch(req, ctx.env); + const text = await res.text(); + const json = text ? JSON.parse(text) : {}; + if (!res.ok) throw new Error(`${method} failed (${res.status}): ${text}`); + return (json as { blob: BlobRef }).blob; +} + +async function mintServiceAuthForUpload( + ctx: AuthedCallContext, + lxm: string, + expSeconds = 60 +): Promise { + const res = await ctx.client.get('com.atproto.server.getServiceAuth', { + params: { + aud: ctx.env.SERVICE_DID as `did:${string}:${string}`, + lxm: lxm as `${string}.${string}.${string}`, + exp: Math.floor(Date.now() / 1000) + expSeconds + } + }); + if (!res.ok) throw new Error('getServiceAuth failed'); + return (res.data as { token: string }).token; +} diff --git a/examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts new file mode 100644 index 0000000..2370b04 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts @@ -0,0 +1,72 @@ +/** Per-space unread tracker. + * - `counts[spaceUri]` is the number of record.created events received since + * the space was last opened. + * - `lastRead[spaceUri]` is the iso timestamp of the last "open" event, + * persisted to localStorage so cold loads can compare against existing + * messages' createdAt to derive initial dot state. + * + * Reset counts call on channel open via `markLastRead()` / `resetUnread()`. */ + +import { untrack } from 'svelte'; + +const LS_KEY = 'rooms:lastRead:v1'; + +function loadLastRead(): Record { + if (typeof localStorage === 'undefined') return {}; + try { + const raw = localStorage.getItem(LS_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + return typeof parsed === 'object' && parsed ? parsed : {}; + } catch { + return {}; + } +} + +function persistLastRead(v: Record) { + if (typeof localStorage === 'undefined') return; + try { + localStorage.setItem(LS_KEY, JSON.stringify(v)); + } catch { + // ignore quota errors + } +} + +export const unread = $state<{ + counts: Record; + lastRead: Record; +}>({ + counts: {}, + lastRead: loadLastRead() +}); + +/** Called when a record.created event for a space arrives while the space is + * not the active one. Increments the unread counter. + * + * Whole body is wrapped in `untrack` so that a caller running inside a + * `$effect` does NOT subscribe to any of the `unread.*` state we read here. + * Writes still notify real dependents — we just stop the current effect from + * subscribing to state it's about to mutate and re-reading it afterwards. */ +export function bumpUnread(spaceUri: string, createdAt?: string) { + untrack(() => { + const last = unread.lastRead[spaceUri]; + if (createdAt && last && createdAt <= last) return; + unread.counts[spaceUri] = (unread.counts[spaceUri] ?? 0) + 1; + }); +} + +/** Called when a channel view mounts or receives new messages while focused. */ +export function markLastRead(spaceUri: string) { + untrack(() => { + const next = { ...unread.lastRead, [spaceUri]: new Date().toISOString() }; + unread.counts[spaceUri] = 0; + unread.lastRead = next; + persistLastRead(next); + }); +} + +export function resetUnread(spaceUri: string) { + untrack(() => { + if (unread.counts[spaceUri]) unread.counts[spaceUri] = 0; + }); +} diff --git a/examples/sveltekit-group-chat/src/lib/rooms/uri.ts b/examples/sveltekit-group-chat/src/lib/rooms/uri.ts new file mode 100644 index 0000000..4a263cf --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/uri.ts @@ -0,0 +1,23 @@ +/** Helpers for assembling space URIs in the `tools.atmo.chat` namespace. */ + +const SPACE_TYPE = 'tools.atmo.chat.space'; + +export function buildSpaceUri(communityDid: string, key: string): string { + return `at://${communityDid}/${SPACE_TYPE}/${key}`; +} + +export function buildMembersUri(communityDid: string): string { + return buildSpaceUri(communityDid, 'members'); +} + +export function buildAdminUri(communityDid: string): string { + return buildSpaceUri(communityDid, '$admin'); +} + +const SPACE_URI_RE = new RegExp(`^at://([^/]+)/${SPACE_TYPE.replace(/\./g, '\\.')}/([^/]+)$`); + +export function parseSpaceUri(uri: string): { communityDid: string; key: string } | null { + const m = SPACE_URI_RE.exec(uri); + if (!m) return null; + return { communityDid: m[1]!, key: m[2]! }; +} diff --git a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts new file mode 100644 index 0000000..28693cd --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts @@ -0,0 +1,56 @@ +/** Svelte 5 binding over `@atmo-dev/contrail/sync`. Turns the framework-agnostic + * subscribable store into a reactive `$state` object that Svelte effects can + * observe in the usual way. + * + * Usage: + * const query = createWatchQuery({ + * url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(spaceUri)}`, + * }); + * // template: {#each query.records as r (r.uri)} ... + * // teardown: $effect(() => () => query.stop()); + */ + +import { createWatchStore, type WatchRecord, type WatchStoreOptions, type WatchStoreStatus } from '@atmo-dev/contrail/sync'; + +export interface WatchQuery { + readonly records: ReadonlyArray; + readonly status: WatchStoreStatus; + readonly error: Error | null; + stop(): void; +} + +export function createWatchQuery(options: WatchStoreOptions): WatchQuery { + const state = $state<{ + records: ReadonlyArray; + status: WatchStoreStatus; + error: Error | null; + }>({ + records: [], + status: 'idle', + error: null + }); + + const store = createWatchStore(options); + const unsub = store.subscribe((s) => { + state.records = s.records; + state.status = s.status; + state.error = s.error; + }); + store.start(); + + return { + get records() { + return state.records; + }, + get status() { + return state.status; + }, + get error() { + return state.error; + }, + stop() { + unsub(); + store.stop(); + } + }; +} diff --git a/examples/sveltekit-group-chat/src/routes/+layout.server.ts b/examples/sveltekit-group-chat/src/routes/+layout.server.ts index 756dc10..c113b0f 100644 --- a/examples/sveltekit-group-chat/src/routes/+layout.server.ts +++ b/examples/sveltekit-group-chat/src/routes/+layout.server.ts @@ -1,6 +1,11 @@ import type { LayoutServerLoad } from './$types'; -import { getServerClient } from '$lib/contrail'; -import { extractProfile } from '$lib/contrail/client'; +import { dispatch } from '$lib/contrail'; + +interface Profile { + handle?: string; + displayName?: string; + avatar?: string; +} export const load: LayoutServerLoad = async ({ locals, platform }) => { if (!locals.did || !locals.client) { @@ -8,17 +13,26 @@ export const load: LayoutServerLoad = async ({ locals, platform }) => { } try { - const client = getServerClient(platform!.env.DB); - const res = await client.get('statusphere.app.getProfile', { - params: { actor: locals.did } - }); - + // tools.atmo.chat.getProfile is unauthenticated. + const req = new Request( + `http://localhost/xrpc/tools.atmo.chat.getProfile?actor=${encodeURIComponent(locals.did)}` + ); + const res = await dispatch(req, platform!.env); if (!res.ok) return { did: locals.did, profile: null }; - - return { - did: locals.did, - profile: extractProfile(res.data) + const data = (await res.json()) as { + profiles?: Array<{ + did: string; + handle?: string | null; + record?: { displayName?: string; avatar?: string }; + }>; + }; + const entry = data.profiles?.[0]; + const profile: Profile = { + handle: entry?.handle ?? undefined, + displayName: entry?.record?.displayName, + avatar: entry?.record?.avatar }; + return { did: locals.did, profile }; } catch { return { did: locals.did, profile: null }; } diff --git a/examples/sveltekit-group-chat/src/routes/+layout.svelte b/examples/sveltekit-group-chat/src/routes/+layout.svelte index 3b11797..4b5779d 100644 --- a/examples/sveltekit-group-chat/src/routes/+layout.svelte +++ b/examples/sveltekit-group-chat/src/routes/+layout.svelte @@ -1,22 +1,33 @@ {@render children()} - + diff --git a/examples/sveltekit-group-chat/src/routes/+page.server.ts b/examples/sveltekit-group-chat/src/routes/+page.server.ts index a9bb158..494c86d 100644 --- a/examples/sveltekit-group-chat/src/routes/+page.server.ts +++ b/examples/sveltekit-group-chat/src/routes/+page.server.ts @@ -1,42 +1,70 @@ -import { getServerClient } from '$lib/contrail'; -import { extractProfile, type Profile } from '$lib/contrail/client'; import type { PageServerLoad } from './$types'; +import type { Client } from '@atcute/client'; +import { authedFetch } from '$lib/rooms/server'; +import { parseSpaceUri } from '$lib/rooms/uri'; -export const load: PageServerLoad = async ({ platform }) => { - try { - const client = getServerClient(platform!.env.DB); +interface ServerEntry { + communityDid: string; + name: string; + description?: string; + iconUrl?: string; + createdAt: string; + membersUri: string; +} - const res = await client.get('statusphere.app.status.listRecords', { - params: { limit: 50, profiles: true, sort: 'createdAt', order: 'desc' } - }); +export const load: PageServerLoad = async ({ locals, platform }) => { + if (!locals.did || !locals.client) { + return { loggedIn: false, servers: [] as ServerEntry[] }; + } - if (!res.ok) return { statuses: [], profiles: {} }; + const ctx = { + env: platform!.env, + client: locals.client as Client, + did: locals.did as string + }; - const records = res.data.records; - const statuses = records - .map((r) => { - const record = r.record as { status: string; createdAt: string }; - return { - did: r.did, - rkey: r.rkey, - status: record.status, - createdAt: record.createdAt + try { + const data = await authedFetch<{ + records: Array<{ + did: string; + rkey: string; + record: { + communityDid?: string; + name?: string; + description?: string; + createdAt?: string; + icon?: { ref?: { $link?: string } }; }; - }) - .filter((s) => !isNaN(new Date(s.createdAt).getTime())); + space?: string; + }>; + }>(ctx, 'tools.atmo.chat.server.listRecords', { query: { limit: '50' } }); - const profiles: Record = {}; - if (res.data.profiles) { - for (const p of res.data.profiles) { - profiles[p.did] = extractProfile(p); - } + const servers: ServerEntry[] = []; + for (const r of data.records) { + if (!r.space) continue; + const parsed = parseSpaceUri(r.space); + if (!parsed) continue; + if (r.did !== parsed.communityDid) continue; + if (parsed.key !== 'members') continue; + if (r.rkey !== 'self') continue; + const rec = r.record; + if (!rec?.communityDid || !rec.name || !rec.createdAt) continue; + const iconCid = rec.icon?.ref?.$link; + servers.push({ + communityDid: rec.communityDid, + name: rec.name, + description: rec.description, + iconUrl: iconCid + ? `/api/blob?spaceUri=${encodeURIComponent(r.space)}&cid=${encodeURIComponent(iconCid)}` + : undefined, + createdAt: rec.createdAt, + membersUri: r.space + }); } + servers.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); - return { statuses, profiles }; + return { loggedIn: true, servers }; } catch { - return { - statuses: [] as { did: string; rkey: string; status: string; createdAt: string }[], - profiles: {} as Record - }; + return { loggedIn: true, servers: [] as ServerEntry[] }; } }; diff --git a/examples/sveltekit-group-chat/src/routes/+page.svelte b/examples/sveltekit-group-chat/src/routes/+page.svelte index 0225014..0e30c4e 100644 --- a/examples/sveltekit-group-chat/src/routes/+page.svelte +++ b/examples/sveltekit-group-chat/src/routes/+page.svelte @@ -1,188 +1,76 @@ -

-

svelte + cloudflare workers statusphere

- - +
+
+ group chat + {#if user.isLoggedIn} +
+ {user.profile?.handle ?? user.did} + +
+ {:else} + + {/if} +
{#if !user.isLoggedIn} - +

+ Sign in with your atproto account to create a server or join one you've been invited to. +

{:else} -
- { - const rkey = createTID(); - const createdAt = new Date().toISOString(); - const key = `${user.did!}-${rkey}`; - seenKeys.add(key); - localStatuses = [ - { did: user.did!, rkey, status: emoji.unicode, createdAt }, - ...localStatuses - ]; - open = false; - await putRecord({ - rkey, - collection: 'xyz.statusphere.status', - record: { - status: emoji.unicode, - createdAt - } - }); - }} - bind:open - > - {#snippet child({ props })} - - {/snippet} - - +
+

Your servers

+
- {/if} - {#if allStatuses.length > 0} -
-
- {#if profile} - {profile.displayName || profile.handle} - {:else} - {status.did.slice(0, 20)}... - {/if} - · - - - -
- - {/each} - +
+ + {/each} + + {/if} {/if}
diff --git a/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts b/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts new file mode 100644 index 0000000..d92ca75 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts @@ -0,0 +1,23 @@ +import { error, json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import type { Client } from '@atcute/client'; +import { uploadBlob } from '$lib/rooms/server'; + +/** POST raw bytes (body = bytes) with ?spaceUri=&mimeType=. + * Returns the atproto blob ref. + * Plain +server.ts so we can pass binary without remote-function JSON schema, + * and so there's no auto-revalidation. */ +export const POST: RequestHandler = async ({ request, url, locals, platform }) => { + if (!locals.did || !locals.client) error(401, 'Not authenticated'); + const spaceUri = url.searchParams.get('spaceUri'); + const mimeType = url.searchParams.get('mimeType') ?? request.headers.get('content-type'); + if (!spaceUri) error(400, 'spaceUri required'); + if (!mimeType) error(400, 'mimeType required'); + + const bytes = new Uint8Array(await request.arrayBuffer()); + const blob = await uploadBlob( + { env: platform!.env, client: locals.client as Client, did: locals.did as string }, + { spaceUri, mimeType, bytes } + ); + return json(blob); +}; diff --git a/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts b/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts new file mode 100644 index 0000000..20031d4 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts @@ -0,0 +1,46 @@ +import { error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { dispatch } from '$lib/contrail'; +import { getSignedCookie } from '$lib/atproto/server/signed-cookie'; + +/** GET ?spaceUri=&cid= → blob bytes. + * Proxies through contrail's space.getBlob endpoint with the caller's + * (cookie-based in dev / JWT in prod) auth attached. */ +export const GET: RequestHandler = async ({ url, locals, platform, request, cookies }) => { + const spaceUri = url.searchParams.get('spaceUri'); + const cid = url.searchParams.get('cid'); + if (!spaceUri || !cid) error(400, 'spaceUri and cid required'); + + const target = new URL('http://localhost/xrpc/tools.atmo.chat.space.getBlob'); + target.searchParams.set('spaceUri', spaceUri); + target.searchParams.set('cid', cid); + + // Forward the session cookie so dev auth override works; in prod the + // caller's OAuth session can mint a real JWT — but for we can't + // attach headers from the browser, so we mint server-side. + const headers: Record = {}; + if (platform!.env.DEV_AUTH === '1') { + const did = getSignedCookie(cookies, 'did'); + if (did) headers['X-Dev-Did'] = did; + } else { + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + const authRes = await locals.client.get('com.atproto.server.getServiceAuth', { + params: { + aud: platform!.env.SERVICE_DID as `did:${string}:${string}`, + lxm: 'tools.atmo.chat.space.getBlob' as `${string}.${string}.${string}`, + exp: Math.floor(Date.now() / 1000) + 120 + } + }); + if (authRes.ok) headers.Authorization = `Bearer ${(authRes.data as { token: string }).token}`; + } + + const proxied = await dispatch(new Request(target, { headers }), platform!.env); + // Pass through status + body + content-type. + return new Response(proxied.body, { + status: proxied.status, + headers: { + 'content-type': proxied.headers.get('content-type') ?? 'application/octet-stream', + 'cache-control': 'private, max-age=3600' + } + }); +}; diff --git a/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts b/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts index 77ff19e..22740e3 100644 --- a/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts +++ b/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts @@ -1,4 +1,4 @@ -import { contrail, ensureInit } from '$lib/contrail'; +import { getContrail } from '$lib/contrail'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ request, platform }) => { @@ -7,9 +7,8 @@ export const POST: RequestHandler = async ({ request, platform }) => { return new Response('Unauthorized', { status: 401 }); } - const db = platform!.env.DB; - await ensureInit(db); - await contrail.ingest({}, db); + const contrail = await getContrail(platform!.env); + await contrail.ingest({}, platform!.env.DB); return new Response('OK'); }; diff --git a/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts b/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts new file mode 100644 index 0000000..9408598 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts @@ -0,0 +1,24 @@ +import { error, json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import type { Client } from '@atcute/client'; +import { getRealtimeTicket } from '$lib/rooms/server'; + +/** POST { topic } → realtime ticket. + * Kept as a plain +server.ts endpoint rather than a remote command so that + * calling it doesn't trigger SvelteKit's auto-revalidation (which would + * cycle with the layout effect that opens the EventSource). */ +export const POST: RequestHandler = async ({ request, locals, platform }) => { + if (!locals.did || !locals.client) error(401, 'Not authenticated'); + const body = (await request.json().catch(() => null)) as { topic?: string } | null; + if (!body?.topic) error(400, 'topic required'); + + const res = await getRealtimeTicket( + { + env: platform!.env, + client: locals.client as Client, + did: locals.did as string + }, + { topic: body.topic } + ); + return json(res); +}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts new file mode 100644 index 0000000..f444a2d --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts @@ -0,0 +1,130 @@ +import type { LayoutServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; +import type { Client } from '@atcute/client'; +import { authedFetch } from '$lib/rooms/server'; +import { parseSpaceUri, buildAdminUri } from '$lib/rooms/uri'; + +interface ServerMeta { + communityDid: string; + name: string; + description?: string; + iconUrl?: string; + membersUri: string; +} + +interface ChannelMeta { + spaceUri: string; + key: string; + name: string; + topic?: string; + visibility: 'public' | 'private'; + createdAt: string; +} + +export const load: LayoutServerLoad = async ({ locals, params, platform }) => { + if (!locals.did || !locals.client) { + throw redirect(302, '/'); + } + const communityDid = decodeURIComponent(params.communityDid); + const ctx = { + env: platform!.env, + client: locals.client as Client, + did: locals.did as string + }; + + // --- fetch server record ------------------------------------------------- + let server: ServerMeta | null = null; + try { + const data = await authedFetch<{ + records: Array<{ + did: string; + rkey: string; + record: { + communityDid?: string; + name?: string; + description?: string; + icon?: { ref?: { $link?: string } }; + }; + space?: string; + }>; + }>(ctx, 'tools.atmo.chat.server.listRecords', { + query: { actor: communityDid, limit: '1' } + }); + const row = data.records.find( + (r) => + r.space && + r.did === communityDid && + r.rkey === 'self' && + r.record?.communityDid === communityDid + ); + if (row?.record?.name) { + const iconCid = row.record.icon?.ref?.$link; + server = { + communityDid, + name: row.record.name, + description: row.record.description, + iconUrl: iconCid + ? `/api/blob?spaceUri=${encodeURIComponent(row.space!)}&cid=${encodeURIComponent(iconCid)}` + : undefined, + membersUri: row.space! + }; + } + } catch { + // fallthrough — server stays null, UI shows "server" fallback + } + + // --- fetch channels ------------------------------------------------------ + const channels: ChannelMeta[] = []; + try { + const data = await authedFetch<{ + records: Array<{ + did: string; + rkey: string; + record: { + communityDid?: string; + name?: string; + topic?: string; + visibility?: 'public' | 'private'; + createdAt?: string; + }; + space?: string; + }>; + }>(ctx, 'tools.atmo.chat.channel.listRecords', { + query: { actor: communityDid, limit: '100' } + }); + for (const r of data.records) { + if (!r.space || r.did !== communityDid || r.rkey !== 'self') continue; + if (r.record?.communityDid !== communityDid) continue; + if (!r.record.name || !r.record.visibility || !r.record.createdAt) continue; + const parsed = parseSpaceUri(r.space); + if (!parsed || parsed.key.startsWith('$') || parsed.key === 'members') continue; + channels.push({ + spaceUri: r.space, + key: parsed.key, + name: r.record.name, + topic: r.record.topic, + visibility: r.record.visibility, + createdAt: r.record.createdAt + }); + } + channels.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); + } catch { + // fallthrough + } + + // --- caller's access level on $admin ----------------------------------- + let isAdmin = false; + try { + const adminUri = buildAdminUri(communityDid); + const d = await authedFetch<{ accessLevel: string | null }>( + ctx, + 'tools.atmo.chat.community.whoami', + { query: { spaceUri: adminUri } } + ); + isAdmin = d.accessLevel === 'admin' || d.accessLevel === 'owner'; + } catch { + // stay false + } + + return { communityDid, server, channels, isAdmin, myDid: locals.did }; +}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte new file mode 100644 index 0000000..34c8e64 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte @@ -0,0 +1,269 @@ + + + +
+ + + {#if currentChannel} +
+ {currentChannel.visibility === 'private' ? '🔒' : '#'} + {currentChannel.name} + {#if currentChannel.topic} + + {/if} +
+ {:else} + {data.server?.name ?? 'server'} + {/if} +
+
+ + +
+
+ + ← all servers + +
+ {#if data.server?.iconUrl} + + {/if} +
{data.server?.name ?? 'server'}
+
+ {#if data.server?.description} +
{data.server.description}
+ {/if} +
+ + + +
+ {#if data.isAdmin} + + Members + + + Invites + + {/if} +
+ {user.profile?.handle ?? user.did} +
+
+
+
+ +
+ {@render children?.()} +
+ + +
+ new channel +
+ + + + {#if newChannelPrivate} + + {/if} + {#if createError} +
{createError}
+ {/if} +
+ + +
+
+
+
diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte new file mode 100644 index 0000000..6a48b07 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte @@ -0,0 +1,27 @@ + + +
+ {#if data.channels.length === 0} +
+

No channels yet.

+ {#if data.isAdmin} +

Click + in the sidebar to create one.

+ {:else} +

Waiting for an admin to create a channel.

+ {/if} +
+ {:else} + loading… + {/if} +
diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts new file mode 100644 index 0000000..5985ba5 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts @@ -0,0 +1,10 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import { buildSpaceUri } from '$lib/rooms/uri'; + +export const load: PageServerLoad = async ({ locals, params }) => { + if (!locals.did || !locals.client) error(401, 'Not authenticated'); + const communityDid = decodeURIComponent(params.communityDid); + const spaceUri = buildSpaceUri(communityDid, params.channelKey); + return { spaceUri, channelKey: params.channelKey, myDid: locals.did }; +}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte new file mode 100644 index 0000000..fa27463 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte @@ -0,0 +1,144 @@ + + +
+
+ {#if query.status === 'connecting' || query.status === 'snapshot'} +
Loading…
+ {:else if messages.length === 0} +
No messages yet. Say hi.
+ {:else} +
    + {#each messages as m (m.rkey)} +
  • +
    + {displayName(m.authorDid)} + · + +
    +
    + {m.text} +
    +
  • + {/each} +
+ {/if} +
+
+ + +
+ + +
+
+ +{#if sendErr} +
+ {sendErr} +
+{/if} diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts new file mode 100644 index 0000000..2acc2d9 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts @@ -0,0 +1,50 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import type { Client } from '@atcute/client'; +import { listCommunityInvites } from '$lib/rooms/server'; +import { buildMembersUri } from '$lib/rooms/uri'; + +export const load: PageServerLoad = async ({ locals, params, platform }) => { + if (!locals.did || !locals.client) error(401, 'Not authenticated'); + const communityDid = decodeURIComponent(params.communityDid); + const membersUri = buildMembersUri(communityDid); + + let invites: Array<{ + id: string; + tokenHash: string; + accessLevel: string; + createdBy: string; + createdAt: number; + expiresAt: number | null; + maxUses: number | null; + usedCount: number; + revoked: boolean; + note: string | null; + }> = []; + try { + const res = await listCommunityInvites( + { + env: platform!.env, + client: locals.client as Client, + did: locals.did as string + }, + { spaceUri: membersUri, includeRevoked: true } + ); + invites = res.invites.map((r) => ({ + id: r.tokenHash.slice(0, 12), + tokenHash: r.tokenHash, + accessLevel: r.accessLevel, + createdBy: r.createdBy, + createdAt: r.createdAt, + expiresAt: r.expiresAt, + maxUses: r.maxUses, + usedCount: r.usedCount, + revoked: r.revokedAt != null, + note: r.note + })); + } catch { + // ignore — page renders with empty list + } + + return { communityDid, membersUri, invites }; +}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte new file mode 100644 index 0000000..dcd6898 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte @@ -0,0 +1,154 @@ + + +
+
+ Invites +

+ Share an invite link to let new members join. They'll be added to this server's + members role on redemption. +

+ +
+ + + + {#if err} +
+ {err} +
+ {/if} +
+ +
+
+ + {#if lastCreated} +
+
+
New invite
+
{lastCreated.url}
+
+ +
+ {/if} + +
+

Active invites

+ {#if data.invites.length === 0} +

No invites yet.

+ {:else} +
    + {#each data.invites as inv (inv.id)} +
  • +
    +
    + {inv.id} + {#if inv.revoked} + + revoked + + {:else if inv.expiresAt && inv.expiresAt < Date.now()} + + expired + + {:else if inv.maxUses != null && inv.usedCount >= inv.maxUses} + + used up + + {/if} +
    +
    + {inv.usedCount} + {inv.maxUses != null ? `/ ${inv.maxUses}` : ''} used + {#if inv.expiresAt} + · expires + {/if} + {#if inv.note}· {inv.note}{/if} +
    +
    + {#if !inv.revoked} + + {/if} +
  • + {/each} +
+ {/if} +
+
+
diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts new file mode 100644 index 0000000..6b68a46 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts @@ -0,0 +1,49 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import type { Client } from '@atcute/client'; +import { authedFetch } from '$lib/rooms/server'; +import { buildMembersUri } from '$lib/rooms/uri'; + +interface MemberRow { + did?: string; + spaceUri?: string; + accessLevel: string; + grantedBy: string; + grantedAt: number; +} + +export const load: PageServerLoad = async ({ locals, params, platform }) => { + if (!locals.did || !locals.client) error(401, 'Not authenticated'); + const communityDid = decodeURIComponent(params.communityDid); + const membersUri = buildMembersUri(communityDid); + const ctx = { + env: platform!.env, + client: locals.client as Client, + did: locals.did as string + }; + + let members: MemberRow[] = []; + try { + const data = await authedFetch<{ + rows?: Array<{ + subject: { did?: string; spaceUri?: string }; + accessLevel: string; + grantedBy: string; + grantedAt: number; + }>; + }>(ctx, 'tools.atmo.chat.community.space.listMembers', { + query: { spaceUri: membersUri } + }); + members = (data.rows ?? []).map((r) => ({ + did: r.subject.did, + spaceUri: r.subject.spaceUri, + accessLevel: r.accessLevel, + grantedBy: r.grantedBy, + grantedAt: r.grantedAt + })); + } catch { + // empty list + } + + return { communityDid, membersUri, members }; +}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte new file mode 100644 index 0000000..cb32af8 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte @@ -0,0 +1,142 @@ + + +
+
+ Members +

+ Add members by handle (alice.bsky.social) or DID. Membership here gives access + to all public channels. +

+ +
+ + +
+ {#if addingErr} +
{addingErr}
+ {/if} + +
+ {#if data.members.length === 0} +

No members yet.

+ {:else} +
    + {#each data.members as m (m.did ?? m.spaceUri)} +
  • +
    + {#if m.did} +
    {displayName(m.did)}
    + {#if handleOf(m.did) && handleOf(m.did) !== displayName(m.did)} +
    @{handleOf(m.did)}
    + {:else} +
    {m.did}
    + {/if} + {:else if m.spaceUri} +
    + Role: {m.spaceUri} +
    + {/if} +
    + granted +
    +
    +
    + {#if m.did} + + + {:else} + {m.accessLevel} + {/if} +
    +
  • + {/each} +
+ {/if} +
+
+
diff --git a/examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte b/examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte new file mode 100644 index 0000000..0b321a3 --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte @@ -0,0 +1,56 @@ + + +
+ Join server + + {#if !user.isLoggedIn} +

+ Sign in with your atproto account to accept this invite. +

+
+ +
+ {:else} +

+ You're signed in as {user.profile?.handle ?? user.did}. + Accepting will add you to the server's members. +

+
+ + +
+ {#if err} +
{err}
+ {/if} + {/if} +
diff --git a/examples/sveltekit-group-chat/src/routes/new/+page.svelte b/examples/sveltekit-group-chat/src/routes/new/+page.svelte new file mode 100644 index 0000000..ba1c63a --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/new/+page.svelte @@ -0,0 +1,155 @@ + + +
+ {#if !user.isLoggedIn} +

Please sign in first.

+ {:else if result} + Server created +

+ This is the one and only chance to save your recovery key. If lost, ownership of this server + cannot be recovered. +

+ +
{JSON.stringify(
+				result.recoveryKey,
+				null,
+				2
+			)}
+ +
+ + +
+ {:else} + New server +
+ + +
+ +
+ Server icon (optional) + PNG, JPG, WEBP, or GIF. Up to 1 MB. + {#if iconFile} + + {/if} +
+
+ {#if errorMsg} +
{errorMsg}
+ {/if} +
+ + +
+
+ {/if} +
diff --git a/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts b/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts index 400502c..7af293f 100644 --- a/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts +++ b/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts @@ -1,13 +1,8 @@ -import { createHandler } from '@atmo-dev/contrail/server'; -import { contrail, ensureInit } from '$lib/contrail'; +import { dispatch } from '$lib/contrail'; import type { RequestHandler } from './$types'; -const handle = createHandler(contrail); - async function handler(request: Request, platform: App.Platform | undefined) { - const db = platform!.env.DB; - await ensureInit(db); - return handle(request, db) as Promise; + return dispatch(request, platform!.env); } export const GET: RequestHandler = async ({ request, platform }) => handler(request, platform); diff --git a/examples/sveltekit-group-chat/static/og.png b/examples/sveltekit-group-chat/static/og.png deleted file mode 100644 index 62cbbc5fad01c8f7605857bca64b1066af12afb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61667 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz*NS;#K6FCKkpFz}$*?!C{p zzHXb_(Z#CP)!94c?%M0`^m1520s;glgjg-T^8EC<()!Km(^3Ni=k1>VU4Gw<&BEzP z*FWE!X}NsHLPu9Y1~AZ=pTyAuW-*-NY!m>q96S{TK+a@f(9mRY0t+#ywmNWtSqqj3 za4>>dQ$&~)!HkAUP6{kwRzMKUFfXoA!$!l5frO;-L-N#g{rg*Hitc~%`MLi@LBYDZ zy8WxX{)Zll^*wa>-rwV8r5_(1-TmjAbZzrT?&Ku9AHROo{QBZ)v3KLfjj4ZsO`G<$ z*Vkvu)~()C^`_37CMPfd{O^(K)6aZO8rRL6*V@|J)umOx?!yKH`~A;bcPV}-o}wGQ zt*)8v)hnq#hqi3FV!b{8|0*p;1_lv9j+PI`Ti#zteEm)K&!jnX<`^&cKR?UxZ+Pg- z{a!m(pX_{m{D1Q9U0Y_&y5BwB=i%Gi+n4#wnlfwFsVU}}w@SXv*(o+{nyS~7gnL{S zKQ~o}b4^?1H+T1%;?^7;AsngV{at)9XhuyV5c|K#1XCe6Nonqk?8b8l~( zT@wxt3i{E?D`jR=^JV@dRqsWa$>bwgK){J+~Rt{34i@!wgSZ@=5)W4?E6 zY`$oD{jC?5mcG3u@Nj&nDg#4<5>ul<9h)mtVyBSmG`;wX8-vyVrNs5iuP=%c6cYMW z{OYHCgTo2_7LgC{?5ox+T{`u8kpRb37EmeR5GTO#Fg-v;VAYBhJ1TSD+*!(9Z}eW> zf0|y|m#<&{dW!!2&)+yfp2NEAmCByRY43k-PTyPoRYXzX-*s?V^Mn;-HOry-Y#>!{ z)6YxV7b+P?23(MS{`K|xxP3Z8_NPykIyF1|xZegUh$e`$IQ@_m;HZ~kajH{@*;VsX zzW(1)pKU1{AD?@Bd!`f1q5o!}qN`z#g218k4GuppD+qjIUU+#`XfqpsqMG!#=kw>y zE1!X)PONk)FR!n!@9Gf4S66m!Ubk-Dwryn_A03@MdGcZayH4%afib@7?pibm>ydY6BCKEoE9-f(SNi(eFW@gJo_tr{3dHwkD`}gj*<;2C+?FFPHLb5dSUi|;~JpOF-mISrv z-BqULZk-hnrJh<_nUwD5 zud}aTym;|uWaOEiEr)l1e{oPG$aK$qn}x}D*H)L``Y_8htM=cgmmdx`%e{MdW{RrU zloj_EFv@P)Rr*>|Qu5{N*TITwUpSxntsdw9B4qjADU&BFZ$4*antQEhU&Ig9`F6GY zDjx1BsD7D~^36ZN;lXV4HxD;&*s>;QDcAm%eKjvmyZm?>5a+&}@9m*oU$d^QbCr+& z6?XE>84n*1j;hbkZf<^FvcTEdxuv(TR&pYPS;{=mC;sA=0;@YE zI!sLQcJ;q>OiV(~ZCo(-WKZM4xy(|tq-?9cyt=Sbc`kFwDi@a)emRQ<`U78Zw@a8ZGBTb$NxA%H%d6AV^^P7p_T%^O%Xw3F#xXE3D6$+{)O~Au?Cw%| z+cF*2Lq@5m0vmhK zvQCTW#cW7;Kkx3O=}Wz*|Jz$W-_e=v`np`dNY@<`)~s2xeBZwG^K%}aS5;IO2rBN9 z$~tyT?a)q%D_4w-4b!+X^}oKhZ&)o68nk2jili(1_SF8qc23sXZcqLE_faAjm*qyA z_xFkb5LbZgR8t*nEemA5ak^WP(9trPLV&dO@NgNI;~(Dc>2+c)qz ziHNrG$*E{1C@i`5XN#n<#>pw#;eOAOjP2@{IJ|iK^+@K?PiM~f{EIF*wvVgov`_u@ z%f)YJnXbOJW?j_PH5DHp#XQ`(IbGd<(yhs%tHWfC^K71`*xK4YTw9{7F1-8o`-a3`Df5TlwO74|M`y|J$ez5b9c|rx3?9Z#abN+k^EKlT)gPH!~cJO+4*I) zBvo5~URu1~V)xDBTidd;Q&Mz3Gkd@G_4Td%_GaegPfzvZ_XzkgFfb^zI7GNytpC&3 zJ)K9w#3PMQ#H#q!l*Nx1{n%kK@rTr*VEg*Cj@+y#XJ;AyJ@WDKnVu^}QG3f|jc+)g zY3IMPtp3lB``7xXhJOCZ$G0`>`ZSZ!ryr{S)!e&d`9=T#qNQ^jBDTlbW@Si8NP+koGk0L-q+VXJv}`K0`@6d#(zBnRpMSA&vdFa+k)I!>npMoPFYgPQS-`0q zy>-#tV=5Z_=G#qIhkM4YTCu9^-JP3NlX@%XS`}#ti;B9gtx5d8aF4@{%b^9izG^2M zgAD|vBqVgAA70tx%AIy<&z_2phw`5Fi2wUHNoZ!{gh~8wZ@jv?`ZM>>#tD3r-+p=7 z+|F}W+wNm(YUB6nr>3WSa~*9gNWPwT_eXHibBC>2SAX39IZ5C4R`}Ibq02W!t&iJV z6Yh2C(j~Js@!NA>hqRmf_&YnH{`ETnJCxz92eR+FY zQs(EKg#{POd1^VKycN^)%8RJ2~nh|yVyBQw9`#f^!4tD88z zRQ3g#B;JYeY@4{aRN88$?Tk5gyx+{kd?E`Hxeg!V{tCAEtyE z2wa+;dp>TjhP3hK^z_ubI|^$L?I^r+I^RO9?)?1Qelrj4_tP*IIAz1G5?|%!cI>gb zkHn|r@?Gz@uQ+icP}!Y@ZG-qFMg|56juzL6ohx4+a_!5#y;b`{%*2TkKR-LOuk^Fp zU)!7?9j$$RK9?0=oSl76*4m)>Yf3rF>|9q^cv~k{@JC&cgLng0T zqZ2-z_uaa6lO`SFXt^J=(V3n9*wIsU8h(zgA{^P*R+WC4pFE2zhUt;FpP!sn#e^r{ z3|yHOuFMQg7oT2rsXzOfwz_)y@o#VaPEYsrs;f}gGTXNL&JWJ6nD7JS`T@iFODiuA85r{|x! za%D;%_vQ~je*8GbyJVjBw>|$C1TFPCvqw`?)16N~Zd=O6B`t@9bk~-=y!7W}Rq6Y8 z@82K)Yn$}MgZ<#tV>PuE|M!)zpZCXm;vt6_;;u}JbBjz4?&6i6k-H>jZ}IWISnF~Z zH_n%u*Tsl>TduF4-`pTX9dpU*IF87_kZqWyQK^d<%w!8iv z+rNsR0bCf0=tk~ZbH9P{%;Ktp`S<0lN{nB$tz_dW02Cb3^Ra&DG}b)V7Q1M(^6ObE>}nSB-GDW5*vn zc;L=2*VfiH<@(ACt4dc#ZOtxPIK3Lx+OG z{rl}BgOZeVRWvm{ZB8%mjJM947-{(F(^KwsNw1`&OtY`Oy0f%U*^XJm#%O#~>XPM+KTBi0m@6!H%5>@Zt#lCwlFU_GHx~}GR-|{o7 zA5Hn#)O>3 z7VE_|#W=+EV}D)0fB1)RK=s^Blc!&QyrKO4iZD^_u$+5)TBmKhQankiaLa7VTB+{_ z3;#IGm^yXlq)D@;P4hbb>)P4bCgJbmZf%K_kh>IgIq%L6IRSwQbEihGjrwZ!^_9`} zpC?;CKUepiGvWF!m22zD-`!p>AADog&d`(;mG9-2y1Mh1pHDeg{rA__lh>}@+3B9~ z|3cg&{|R%Zgxr60f49u+?HP`au|GDRJ#k`zv5k&Sxx7@!sw*o3lOOgoU0)v?y(6LS z$n^v@b%9Hc3x(AT4Hu?(PW|-s^!eGwzkh5@{>1!nM&aYN>b{TcD^GkZC@(I(Ibr6< z_rVKF4qbN2$Tyzcx|e%*`T5UZS6^545?NkuUHWp(+OzjQowXPH#H_gV!jU5`R_rfw zt^{1)uwr&~#QYyNSM~Hu8I3_pYyN0DJUyUl-s%}&Bgijozr$py;pyIeLw%?rl)6|-lU!T z^ZbS2NpG%cSN0iB$a6k&q-I0brIzT6HSFzyidt;G!R7qJWYHNbLu-1e_94&8eZC{>h6uOkd@n!bc zRZ|)Dxzl@cP zz=uhb6jS|9g`5tvD4ZMd>Eh31`)sl8?e#edGS+o}=Is33Vtnb^*}2BQ-$v^RyfV*D z+nVsuX=b>fYU2dIYO9qm?myL@^NU~RgpZqV%E`a&SAINOH~)z=$JHxWV(!{nmApAI zzv0`?-K(`_7x(nsadFZ9`}q9+v@^4gwtPF@FK4M&QlYr7OIuyQ?(5gPyHbl|Vq&7A zX0^4sc@>@N7XP>N`03-*cM2P+sU6EZ)&r`LJF5+s?oG_e*>kRv`EsnmTzg+BGap}H z!IP@R=g-fv`}<>K{f_8|?n=H&bb&(2COZsULc zZ=QMm!515oXR2P<`D9DY&6C3FJL0;(eED+Bf6kBZ8y@<+S$@~);IX4OXI9S)S6I?h z{`1mNZ*l!g(-n0anH~n;U%qjpVG&c}VYlAx@9${7k$>@RZS)=1bkCCq9Gk<|#2lSd z9rD&t+5OVQMa7;fc4AZ3rJvc9Y-e4fBNcJw@vc`Bg*h~zot>L2FDB;p+#yfl%7q14 z0Y7Iy5J=n0$jBI+tLAC-a%uVPt=ao?pZldXvVVI&+rE5Vfhz+8gAhlHE1xOTiid}u zC|z0tUmSE_ib8KpIDMz&FP z`TMvR8ybH9z9{^j>ymec{NH_*o7H^oYQ*Lg6sF!XFSoT_X?ZmE&hLkh1x~G*vG?wp z+uQS=MH^n)R=Rpa`Fof9s|=VP>RA8ooUCTpy4-*6k}}K7#eY8tDvMd^J)0iTvN*Q( z>8I3*OpPDT@BbgSsYKHF;O_E;Z&}X1=99I)`0`*(($WmC*NZCDeqC%VXkMMY{(iM_ zv8tL%z`Y&i@AdW*oDf{L;HccPbwJFburcIDs$8dv)5zrr9(9|9QP$_4E?Cze~@bJM;3g zhNEHdsXxAnzqjT7iMC2LGBw>>_E{`=NAdAK)xPc>`(M^9-t6~mkGy@|lG?h*ze?Xd z%{{-n?NYMenQfb`i+%`v-~J)lJH{-hAa47#_(iK$c^#beQ%j&zU+B`V9X?aT6=HTI z{JUpc-SV`O_0T=r>Y&Qzwl@!iMYd$bu8-gQNc^3B#fHl6X@4f|={vgUs5cvr%n2Fp zlQ$=YD=aB)`1W((-s+j3j8ae8eEpox)ml0Ahu!z&x*m(#S5w|jIjO|J(6C2QK+N@u z#H)LY*^3MN|NXwdCv)-r&(9})TQ_Z*SaD&$RJvk3`<=yUjorJeXPf2z6uz^&dipJa z&d$y|-qycw3NLZzIC1989GjEBUQN9dR+n4+xmaB9)BV4no}N}>XP;@VQ2PGf;oQe= zb)G&xJZrmOulReM?a-;Vw(@uPgu~w4uy=30tHq`5c5O}6(}(ZW?rDe^}oJ$h={l`%~$&S`_r=0ZfOcjn71d~vsrdur0+wWU%kEktuq%c1f-qX z^z_uuUAwpE-EE7XF=4`m75kQI*q{F!pW$IIUHR|MO-(biFP2v?Tv$-Nta$qU9Yw#o zGy?Zjc-|8|ZTan6JHM<*=lL_@jq87i8^8SQ5xTeXaht~WXpzOU%WY-u}+_ioy~2%@82zEHcqf+f6?-Motbfylhe%qf-fB2+}rv1 z#l^)76%{A(bG%J?X0fmS|Gd|=QVa|V`k-d-<4H@I*?0mb|0}ipp1E|#4srcB7dN*( zbzgs-ntJ--rNt8-N&gPWmQJHh=&skCB-{0O8Mog%0H_?yZ8@A{0 z`8^s%ooy_8>JER2)hxb}bMC`LWi81+@3%ADn)&?PyxO0iQuj3mL`PSDy;XXqw!cIv z(0aDL&zzRGUnGN<`8}O}VcoJ${WN?RUc(c!OzqwggRvetKbKhaclqoKz z`(K}Yw)E1{s~X1)j(1Nhdwq=e9h6-_@VIi`< zcUeCU|Ng$k(m3ths;{LM?aTM=+cG`tqg(lelDf-FGB+P~@BbwJ>D5*3s1wp3b9;@> z`|y`=*4cTIKC~{$UGoTVTr-{yX(*C`}q3KwWyo( zA>u;%^G^NWlA0GbT9kh~lk!n~(;0^u*DViJzIm%GGi@^8yVuv(H{D}7dhCczMZy1r ze}7Ms|INU_Akyv-Arfk8Jlnk9@i$*i|AL;Lo}TOLd?pJ<12Qa*?VVGZuIhVZ(^gn79Zo< zn!mzR7gRp~Y2o}V1CG73P4vS1-nO|ct6@%z6Q6bD@UO~n427wy<#aVvb1rfR=l{;f?*Pv-}H zKXobmY{~7FcYc!o6 z?(#YrF)#Efe)E=3-bzJhdGMD96Bi#UI_i1OAJoe!ZZJ+<^i?D}`q{r+MXnEwjEv#y z;}-rGY`V86QEx3X1H%c>z)zs?hYJ@Mb1j?r}+pgG0u` z^@p2kXMcWv?pDO}`CIFv6xKvmG%upg>pX7ky!F;Oq(iReR@ zNB2eE+1DHqd3Sen_33A41s^`NHxSq+*xz=XXJ=u-|9_RmOO;w$CQkf#*1CMgq4@$F z$Ngs*BwSda71|>r-xaXLL19n*d%3w2{>U|0Zq2^FY;IcA)>SX8d0t!(FA8>u(}-DH z5aszZILIlosqER?=CUSD5VSo*U(W&h64&-*2{zixbbzfMP`XL<^#4|`AGBv(u3%FHcWqA&RidTYpW zXfpFl9w~Ei`StDf_OiX3H^da$-Md#ZW$_}VRJKEV&N1@U3bpgf8|M9~U}ayvH2=qG zas5l}-v44%7#J9oSPm(5|GPdvDcid0i$p0euj{h^KRzm(-n|nO`{(bUA5Xc(^kgiv zO!l`pJ^r(^`2F3z*5&`23@_RKi``XmvF@5r*YrE5v(58nhE08ScGChbO#v&ND3hco zE8ef1F^A*G_sdL)lb8AObIU!vy{GW8*K$$T*tvfkES~h+tNG0F$n5&RZjSfFTTf5h ze@v|YVp;U#gIoFc3l}EHOK{xYmhJvN%VYlA-{0TG8pONpU+fYe|9_ssO0hD9Em`+! z=6+^NJiq$qDRY5)yUV8^a!7Jw3f7Zbdb!y$j@7i}!h&}Go44~MO|!4gvP*7z;^;Wh z;@R1a+xOSM*OL`LIbmvGVBpTbH>;|vD}PUUe)ph7)sqv|<+gw0*bE9&f84LX==kto zS;P~Iq$eK#cKfuO@PwCtm$R)lY%P(xd{SqUC`WK`aHjmdfA2t@#jOPgYaY5f{d%Jl z|8UCtH$^tJzou+6SF(voQ#kWl=0SkM`k0-cYS`HmJ&)ux zJh|j#@QeGa($a1<9X#7|=^M4;&QJUHYqfftk`)j?0%WiLCrH{rvptdXaB5_w{^EW`6iYGkE#g86VGz zDx0s1FOJ%0^Z#VteNFA`85N?Tp$}g?;rzI%{?`3%xv!7?Ve`CxMd$OG`EOl6{MeX$ z$Do8&$zEVv=Iyd+du!)f1>M~kv$JOAt(Aw)1-fjq5>pa+s1Xo&daFa@gjw%PYG&#Q zs5Va6B(^tt-<~Tka*Mt>EOzVd|8y?YZy}o#m+O~_E0ykk2q@HRaTVECeOt2Q^#1HGmyUL`^S^ms zzQX%q?x!W5FJESAhpm&cN$E&AwYU0PfY9w-we0mXw`iZq{`Wre)XCswKC>0S`WTf9 ze)_c3+h}#kgA0jP+DpuzZvOG}>gPp=cv>Eb)=HYEo!PoN=y}=gvbW{$(tcmoy0Ee0 z;rsnLCm-FoaU-9jMW|$khWd50^=sGXFF0{N^!B#A*quo`&n%Z=WMHu1XmORjp3TbY zDfOI9aOkKY>`3oblR^t&V*D6%3+>0`xrdAphu z<(q!-O0Ae0COj?r+WPZ0=Y_vK>pfg^bMKWswbEby{rz2Y{p+t>gW}So_s_2n=P&zF z(Km6TpuP3Qb3B}!5B1)j;%`w?Tjo7oKfUPR=jZ2F#_AQz^fflKt&iC|OLI<|_18Bq zFJCz_MUo@`-VR4^;j-_yRu-pQ?K?Af+9U^$-Ba$R{>s~1`uUkmb#>Yn(Q`KnA3r;j zTwbw|?NFw1+6|w1hrWFANEfWVld*To#3PLclY=fySsDCu`Yo6Hf*f17Zq3|oGX34V zr@yvlv+gMQqxzjEM1`NjxYEU?<@2tstFNt@n)7wv-fGz+*K>Yl`hgSeNUm!12zOTK%{l`Sh)c-(sRp0h=%Fx)xq>KP4zu$4(S zRu)f>+L}FGJ6vylv}gRj3P)>Qiy5`E7#J8Fm>M^nSokgJ=BB4!Q>Og7Z)>Xh^zrff z-+vYybD6uVw7pm2;znn8-GixzbECKG#P6CCuO#!JZ`Rr?tCHO!_7(j5v!qk_$K*h> z+j~M+owmzjxOsL?;h!6ZGfl5wy{daq+S^LqTHVmF@%7aQznLB;#mCFnEo^OldeS(( zMRYOisXb?wE)I5d4b6(l&no5o{U9(VvB&wxh1Z;K9qKquA4QvFpX$AK)_v{ggk=~r9P{}_Nv`=5qoObAZ;4a{_kV^-JEZY4xS1oW@gJ`qui@hI$K(PH2>yb6TQ3b z^;8!pZ|~Db&&J+3nX-`W5chPw>uXfMdg!W7I45hpr@!x+c-Qw0+?y}&vlO^=$!y0i zL+8ErH9s2E`h9N3lq($% z?-(ER*|&&y$>pmp*FD!1bj;fI_0?Rv({>uKAHTi3KVN=g!C#3pZPIXor8!m2}qr}%j3VYWKX zt!85SXW#zqpZWK|zS_^vJx%knuB<5hcq%l_C|-X4uRVtMR~VXSKkJ!%>HC8~?Up9? z&k^Bqwt5m0A&bfk>;-~er@x(b_56HW;aYcvFWUf%lG=0{x00_$S-rae$7<1j4`5ASmJ*)DL1&+O#~ z-+3u>!`s&>y=;7`?myQm_ZLqZQ`Xbq==qvCH&zJds(n6jXLtF+D>+|<`};iFAN=Gl z`{K&AwD`^284GvSzrJ<0@T8Eft!-M8r$R_*=+Y;Ld!FkF@xQdYS@p>KrJ-D=yKYI$ znVw&|T2CBaJbiq+rNGLvx-|W`*!rwAw}Xqr*53MaD4(w-<<*wV;M}F}W-kpeJbdlj zsi~*WOc60wi-<0NaicT-hoSA?_xAq<^um^jYKQa58BK|wG;yNhP9eYLGhJni-%Zz) zm6f$JpA{Kdm|wX6_mn5Kf0C1v-lUzK@+Cl3sQ$qGAHTA)yzVypo}QCr+_{@Oq-omY z4H*~X>%H&JbC9#IaydTzcjuFKmP22@l-yFfb->xZFDNcGZQ=VBFSiBzf3CW@x8!Tq z*H=NzT~A7NGK7!$&Nv{`9c`YsW7m_qUU3tFK1u6!pYwCt&#w+&`>9IhtHj!E+rCZB zzJA)f-%oM>b2$bE20^AoL6yzry%#QA_?CO)wc?>@tCQDsqr-G2?{e~ZyP=w2-Y_{L zH8u5P{=La1hUazOp14>qWuALVf8n!#CmtS_yR-kizE0qxBQ2fRjML6O5^rm7zic$$ z_N?qq?}-i`+zZ8|RoPdzwzTHo+>?1fuB;<#rl@tvgNDb8T7QQ1Sm?~ypU=PB=cuTg zg2=bu`aI$^iTBYM$7vS8Ht&NpT0zO|}9`GK3Ka>pL+60H`NIdsQWQ|NuKp|ZP2)68}2<{e+= z+bd-*mbXqlQ4lVur#6dz<^zI?A zS1KyS&(8hMzG0BYu=hP@3ux_8*&~(xEoH2-uP-XQ8|)XkvL^EK(Ytd!b>2HU-{#zB z^+~DTVfv06H`L^6}%xUg;-%FAZysoOo%RK26ZZ zz{fIc+ub!c){8Sd*Nrf!>ba7;u~N1>ZvXdfGmU0iidC1rk9zVzL90Sx%FW(uZv6dM zwQu_6wHy{*rlNj1>HVF9qD~A9Cxn?2Cnl@xD_^(n%idz8!rLXsx2>JRV{Bx+Wy_Ye zYu2dw&Pz%;QdVeKIqB=p%{j&U{rA_seo{80?&l@d+nSSG%LB_^->ZFFm7exNJFDRS z?&|M9HYRV}X!vDX=gQ3GeseXC8znw_F;Thr&D$NbXNhpOq@3OL_3y9E8?_7+4&1$~ zYow&4uHN1~y)x8CW#8%U)e_~;-^$nj(_ABQb$?*(>GzTgR~s-TZq2$HbH_I8YSQta z3#x)ve>w6o`O1SUSFX%3DxEdau=+y&{avorMvix;oG}-eR4lH4ZQWV6RsAnt-n(~i z%c3t|zIaX53O2hS8goII^U{~7oULC&v+78dlo0_EWS^ROxNK7pJ zb0zaxLg77k_xn2vZS~ChiV{AH>CV`p8n~kI!eNkgw`pLz|hbMGp%+4ZV zbzdX%^~yp&<~huW?mZozb!E*-5j8cnb&*?*EG#UvwY$sKJ1+GqettH6-K?OpSNFGn z-@N>MieA`?q@!KmRn*n$&xB|2bi6K2-gAZ@N)lh(q(G zsi=EQXz0~%vQ`#rqt1S5@|NCIeEgg2?QO1I>_s_gsZ*_^mL)X&Q1{>S?cF_DQOTb_ zK@E&4Uw(agdpw6pw%hOR?ZU%6VkQFH@-C-+zgO#%7!`Hvn(plLb1kKXKQFBKC^VH< zyZptCz>1IY&vfrFFfi1B#+tW^Ki7@8u>U_pnvYr5tRpo)6gXR^$hVwL%HrCdcT>qy zvr}{VvXjdC?~9_Ix>QDNy_7k*y?w);nxCJh?Eb+o=vnjU$HZBMA}3dHs^%OCz9(%} zapCd$auZDnj^}4)o0rYGvLRo_Yt@!@bKbl$TN|YnVOXf3qy46~{o3yI@9&OY5*OlL zWx%BP_V7XHHoa)Y-OXHCzLz);>{sp2zt$sp{NK;-T8oq#H-x{x=RHktvgD;54Dmib zKi2J^fB%6a^X1CDJI|FzSY&<4csPaY5xdhZ=HGlWDOS^TCOGl7Tuwi;VWv^&j!SFK zoM}1YV!HLxtXZik%OWq9vpac-cTd;3C=#=O(Pkw}%bDCQA;k}G*}lIc`R)3Aqv0B=W$bKz0j1<2#SubKD(>*t@VcUI}0<4U#vNu zE$_E!Ud78ptS5tu?ac(bj(oSb)+rLXx6L=+Zg=_HsGUmNgOf~4zyACz{^i%-@`oEw zoIAB>y{w+VsXqss>UH)#lsuGeo-ZX4l5{FFKj2b#XXirJFf)@iF*`SX(0=r!Q@Hre zTO(uR)^1h?h6f6uQR8_tY)W6XbpGt0b52}R^5LUB4^JeP{m}7?i}e1I|9{cf;Nakt zeXEx*FMo5y^Iyl+pNqtPzMAUy``f?wingrGiN@O6T`ZNQ)7?8yoOx2uyeaT#*}Hqn zpRYOo6t%8?#$$R{WWM(b1181eN6)U!-u|wV)qdfeIddi-_rG!W*ZovRS0+W%-N*an z^V99E*B{&Nb1?t@Ex(y(?yzKJW?EL?+mm*7&6D%qxwTmlt5>Zun(q6fF+gRX?WMDK z4=)h9{XvwAhhv52j#*JF8ZX?rbLZ&Yy;-5gKSdYo{+%*;vDV%>?|E8EW~ljXnrT*B z)!28v`Sd5D{r=3qO<=Exl4?&juRXn@%^Ob29$1^eB->>!kKgLck`EksDi{6Ee z3x3JlY^Z+ChaYlAsx#H*M$3NQ9VSlZusd2)z`frC=AKvYmr23gz@#T$`mwl{vOPOYA z1sP7P=&)Iqbbp!a>1k8$oSOPOUcBl1yDGl_zu&))S>ti1an<2Pu2YIW@6w+1@z2f8 zH@}tX$M2aiMMH?gHD0`fcsn_&ZS9X5>Bcl?H zDZ+Ac|30Ib0vjwv4qki;9RacwF{G)zmb#Z|~j@?>^S>${B&Ctsb&> zo!fF}qvxSRKU&-S<#I1=nOUIt{M@{abB$EjZ`5n!m3Pcu8M=F0lYI1f>+)%Ok$Yqj?91;TyK!X6>HPhHt3&ES7GL`|@9u7XS(^{fzGm08i*d0z1qH^}FWOqV{oUQv zce$KVKOP_N|7ThB<%Yfc%z4HZvu4fu%f?d>u|s2hw%SfxL%+>Sxz@+N*RdDqI#T|0 zYsSSxXYVeavLNu>$=GwRuB!Xb+wr~OE%V9%X2tDGKZ{v>d3`_r8NarQl1{DB`=Z3ks zZo0mMOH<%kseV;el~wEN!|eP;`Y$7VHg1(Qsp$Cg>+(j6+E-V$t&N`H{$pRRjMFcd zEqQmVUT%8Y)w8VP{XN_8^ylsT@=vT)85kIt5(BRZvA-|HV6jKn>A}w`FXi|dwW-{ z;Pvxd_cQ)|eQmY-+oPLOi{z&+Y?&}|=EO;pe!Yu*(VKnUU&dZdQh(z5Z99Le%s%;N z()s!JmA2n*ZLXeP@aWF{L%Yl0U)kBcaN)vv^W@4U6`x;L&-$=6`>vPYQ6C>4UM)!v zEw{}9AP{QUhJ2% zwJCffAzS;f=TNM!Q2$BA;_&VxcU}2pf4P62rrX=wYieR*a(aqp+?t58va%vCubP+` zkAe^HVt2>wt5GbD+t7$CS^u%zdi?R@`I`Cf?rgNRvf5Sk_0{6z zJ*jg0tjfRf7(ZcfcVB+!g8lymXRiw_&h_{8T-e&u*7D}w-q%mN#YI+Y>^Cg_`TO_V z8{2yS@3=Byp>y=swDonD*9nxZ$WBbOta;+0CSX_hWrErKyO-R0H4IF$uIy-9x2|nh zRalvs>7$ns0Su3~rGA*8xc2szr{|sIubvjyFJgWXC{a*YoIcGndQ0u?Z9P1a^JUp` zf5abdi;OfiUlFo0=H{j?_Ai;8s;#ZHU&ls7R7A|!JzIPK@pDa=u3nb5P2;(Ic)7;* z`zyQuFiKb$l}jz0AiwB<$BV65R~Hp8D|>s(%F1eI|Gkx~gPG2pI_1;hU-94W*Q=?k zx98vQnov4vvf<-n*Vo6-k9V4xdwknkMn*=X<>rQllBQ`o#g)DLBPNoncBUj?-iQ&R3M40g8?3+-7oMK{_nIQa8siCXFQz18lS#_N+ExtGt4+20r< z7WzSE0l#Lgty%uXO+`<=M73XC_UfLl;iw*(=EnB6aeMy#x{Ck*UR_;%ef>sfUPcCn zC#;Ph{WV_G5{>J$wD(~ZWVL0mz312M-CA-*4ExK9GVx78a5v~?dZO$YoegOz%wIS1+e676@{iEYFddFFx__VvAdo zr~E%XP4}~@K-$Gh=lDF4{JPSsB2LGZzL#roJ-^gwpsTw!px?EXOH?at%?a+7nJ-^v zDk>`ekN+mkz#t+!WcOV)97?^qXXmtOX8TnvHDBJ_yLs^$?fr>@?c3$mP7w{REddMSy}y=bmhvG>+9pyRn^4qd3B3whuyvt*;j5V*|>Q9`h0Y)e2pKbJA*gQ zL5xD<|8Kvv)H{4_RA{Vo^FNKSH4$lPXc{Vm>g8@;5PA8a5H zwkl+$Q!AI$tXWe&+SUJ>Gi}UF`etH0mh>&(|& zX%Fr-K});V&70@tIkjKj{@0I+;G->*SFTv~>gB7j3s;xUTD@Y$vZYJa)YXq4J*K9j zQggcsw6*ZTb7uveS6b3ivu4c-TbXoaWpR3JY;11s+oYr2!KbaYlpbxo{y4mG{k=VP zu8hr{=682}*NEFA@z*fFv3cJWp+R0N12?N3?NRfc zeRWmn>ZGGxQ(QN1-eh0%LqJfj?B}VcKX&X8|MxG1_g_rvscE@4HY{AWEUi;W)xB3T zIWh5Iz4?!~w;vzx3lv(sf90m<=jT2>C2gMRbo#mf%&AkqUb#N~n(7_t*t*}nm(=#F zXll+hQ}ms%z-xNg>e$_7y|YYbndc`)EbS_*%r!Or`eezI8#iXWc=2L&__~(8)z9C* zSC45e`}s*XYRiM^7xsOvdn79>8@i|BW7O6x^PD>?+bE;YluXr$+?2ij-QweYOC2JX`ADAPKa{y*=l@rs z4<7_S+HuHQ&By?ho+x&c4^gdUnmMV1+3=C-~1V(r63Z zUH|ivsz`#$QxU&AHa0!IzMo?CU3H9wli$6+`}^D0yt{4x^Sp{aeaYN*eO;XBxfk*R zE0!%grq9X4vti@5v_lcM%idOfe|dXNq;V}X^UUA6>gwuBHh&I)wkW*u_w{|c|LUgH z)qC0bK6u;J|MOYBqvGM8j)}_u;=GF2088=VPe6FIVCU<9l{pn}EK2M&lj$VIn#>U4wucM`;!WOn1+vCmc z>F2ep`un?Yxwkb8xYNs{wpLwuaPZHh2@f9VMw`73yD&E)_w4Gf4hiU_Sb(KAs{P^7e`_W$1A;Mmx`o|l((i|HGhnT4e+6$!t#E^=@A z_q=zit|I3By>4!9pPmbOvLC8t=i8Q{`PCsdE9+EA^zGH*DMvcR8`o!?|KJ_Bzshyy zx390SPx6(pNqN!K$}PImeqn_7&ew;2ex5YREcf=iTbrk^e0%5S=CiA21~N?EtkKo_ z``f$7ok^}~X=?jp(iHX-zrMCW(OKj3>Bt9rtG~PTOPtg3RoUlYp7mu$=VZ0OlIi

rDKk6oqv?C<@)9#o#xzf5anj1qN`1PqFnFd>>ZjQJ zu&`_Y_Ez)D+kMe8Fu30dE=|H66?7K4^~kucbUSFGaVoby{oSpN#pmDMoj6f&Q9f_W z-Rf^!Hg7jK3ew;$5)u}6=aoNJDjF=pl=z!Z_S(AgFcnQt7Z;Z=?=MFewXZtkAR!^K zAlfl}xfAQp?q7d?e!iwVTT}QGcWa}8sp-}!t5$Wb%Gy_2Ji|W!{=L0EiSO-d=Y*}* z3tbhmZ?pG>&W%M!KlRJodwI1qeh6MC6B=5!agxK0Es@60dG&s=v2m@N@~!Ru=jVTa zeVO+Eh$g6p`s2jW;u_e@#{1mT!X)R$28ZIOPoDV9wt9N!=AHD<`*a2FecO{AvDC{k zyZY0SkCK;{1!ie+HGY`?SY662E98RjjeWVVH>rB>*x%Z?VS!con*;k>LnSywzdz}` z9^`&}`JU>k)e@3BD=V(7PFGCVTexP;nv;{)tdH5JqbbOCXpaZe!c+CR6?`lX1=L zTIuQLc5HL{`M6)d(^Jzg7tJ!uPD@KWw#LN7BxUjL-xCkF^YX6lk+$ErXkXpOM@MyZ zbV|Q|`7v2lRgH~}tw{Dp&A&gHkA8fN`N_}IqNSy!AHO$E?qqj=w{`u$J4?I8ot>Rt zUs?OR`u)8vqM)`z=Ddo^i%vV%1wUawG-b+^@bz&|Q_^3DUJ|lXGS`JGg)Tx3_wsqGD$! z=azM4Z*O1Tm>~A`+1J;4bFIAMPI0!(x3BjT3)YO>l(Vy_w(h;_&u!`J!@4R&Ra{u4rl7~&KN*^ER zPC2eIBe$UR?TwYj^+xjf`CMlH^Pk;|`2}8E?QZm=v2lZ2?<{fF;OVQNnq_lk>u0T4p%KM#H9*B&;McDoJ9h8<`O{XM{ZLo!@2}EkH7%@U%;?7Whf*4NiRTj^wV=lc5C**2L$ZpZeQSKC!R zigo;T?&_K~Yc?)+x34L%f1SO4$rk(ee;?EP=h@D5{rcwG+oz}NcehUz<_KRO|Qyt!64-KvB|WM}+iXFF8( z_V)W5TW9~*3$5d2b>d$cEv8xW_U88a_np(cyt+Q`+FDn)b8)~EQI7k2DlhL&=l_>n zA<}rPPujlvT}+x6mq&1pTI{}(&T36#fq7PyO_vffJXS1eDZ4c(_4Len_O6fe@-vu~ zm5StfmukHJ@%365-_f%+fU4IV6T9@lZZ;H6F`qjtD%U_vi9ME651T+f^K0HFimZQb> zB3tWPRd*2R;|L)Bb&&evP`?UnZR>%GQ6k4|X(c{PZv3m-hUCGp6e|!?-wbQR( zOnGoO((CDz)*U&@#sbThFV`=ODfw|haal}o@aJ!Ls}I@bXe`OSziaC6=rn~l_m}tg z_nQZ2MBe{&WTmlBVnBdEZbE_s!{6G!&vb=@ga7S*@8tY4R_~P2G`;wL$L9y_=ex6~ z@b0l&v$nZTaaY(={9DY|;p2>WkLL?FcV~lARCVF*zt>t2b<5%Hv9Ser{VPXEh79aMv`GGgI0&-Zq7=Z z%A+;)@}*TFCl9QND$UK!{b%B;E)c}QStC5tRzo_ywxYxJ$X?)=LNzORezRy6BW*!J7-t=Y02F)l4puEzFac9l$UtQxv4od zk7@0c%9<%5rswvr2A}Bhe_o7v4X`i3zXWU=d{O6>Rk&y;REPQq3mmmvYN9KjQ z+S*!MQy07OT9>>^$%}KJxRJNZPeJ8%Y)t+CzvVw|*95g{rrzCE`uWz?)ph6RtE;Gd zDBgO;VMoEiruu)DzXEG=dmT4+9TE+$_;`r*HRu0tvy^#SHg4SLD}C~vQ`nBiNe&*4 z(;uySq&$&huV{P@k3jr2VYWjzX3VhNUA}YYE~Da5Gn10fA2w`R^Z)E?Ro&UquC8u% z=Y>GUtY}I;tE#RZzpv!t;j^|XEqgP97dYHFbL!Rm{q2!kGXnS3 zR32R&wS*~gZPeCl`30b1Mno#I2C1^HU!Hq= z*U`$mR_`9}Qj+Fa9b&la>C2TTo;`W`_xJZLE7arqT{kf;O!*;@w(i!mXFD4Me%(CU zBxRLx;cz?uzI0AdkOfExaCDoTGFuz9eO>hSxaE4WtF#I_)hsP{E)CthWs8YtXH(;b zq@!KZF?VN~Wvj)gJ>C0ciHg_km-6;%M~(-^Ec|64{O5+ljA^s1&X%n@j1&toc(l6_pE4@9v-8`%leSK(DCa$MY8xOJ;8G;gj1_ zbTw;Pz(3u^_r*9e*R$>8Oj_38*Y)Y+$BFw(|IfCts`|Zc?ZiiX-lF_1ZEc}U`aeH5`pw;SI&94Y|Ide+ z6T`x;T{&qsT`w+WSx9j3=W59VF<&N2a#*IGo@Q#kesQql@_DX%m-((evnurT>iKfF zT2j5Iy}7Z^cZ$1$P3fnW)!p4G8IJpodQboR_4|FFhXV{n)X0^*|H@bZU?mE_EaFPOmb|wy{+{0r={Mi`l&33jvSeJdAVU@!GQyg ztFLXaUHsVlOzW&jhONc}K_peRUH*3^#R&R)Tv?JqrpR7+}|Gd31 zmCk31e>4UJ2M7Dc&i$|Vc1y@T1uoO9Yj5r@=YQ&-D6YB1`B;yX=Ber%oU8sUY-XQP zbk-;}ulkC1^mNqKv(UE!fN0+)Vn zJ9u?%b-$dC$op$cy%QGnB&uiZEs6Tnob6XumXovQ%v(@Ps3D#kWuZ>5-h;~LeXrYi z|4h2H^s1PCtX0AThqQ0ZzrUTG|9$_$ORbF)Hl5wFGI)K=z9afuHqW;`cxzVVl*+SG zKIfOJdQFMgcBoU*CibAii}&yMS6w}I+dSVWJ92kk?frj$XC7K$o^Mq?!Eq!s*MxEvW!!^LO(zIQ(il-!g_uD{D~hY#%JUt6@C51`|rS)C!N!;ANcpzrIO$0 zznR91@AGVbl=lV&Ntq`yy|qd^Rl~mQ|1$@RxL-0*xM;Qaz-f+ z9w`2qw0z&b?CWd%#P0{LsZdY}Uhez*_xr@BOWRl8*_(a+($T&{uj@)Umfi9Te#qND zaq7&U$0rNRam%*sz9 z(YK%JMsZkoK8#l^iP}1gbG7Ellc%TY*%n1)IsANOniVHxv!LR~g@cxlvYv=?bgu93 zZ=XGT|Ni)M%l&tWmi*>@;cw{aT9x1+^x>0g=*zdax8FG}FTrtASUx`YrCoxiaFoBq z`^w|Nld8Xd`EqM(zJ2Pd+@+_c{tSM1Z|mj4hfeR_GqE~(J@(TMFa7t{yzb%getGl! zTRTcWr=8rIZECtzOgCCDa$8J%{CxYTkKOy(_+=D|t3eIE2iKhxbc%XEt&N_p zxxeD!p}yQ`XJ==$#UFj&Z7V04x@w+0{q>x5)%UljeLg=t+`e=3+sQ)0moHy#Zfe@J zX%l4mwXw0J>fWc`(}Vtj_Ves4dfL;|lYV~g=_QU6*Eo1wTN8cXzu;~^)7ss;qf=a1 zQfH+5&o64-|NZ6Wvb$9elk97MeRcoTTb~E_%a)`u~ixF4-;!IBfOoFuSM1ny|IAX3df^&iC^2dOb~d(&R}h zs>#QD=NsqGE6|mcl%Fg#`Ox9R&(F^M{3(|$R?pwaekiOlcw9737BQxD!E(r{8U@pnVym*8uOMe zUmj?XJng{39XD3iR&)KobGnJmNmh35<>%)lB|?POeNV31Wg(EYDm%l#dfJ}TYyZ3K ze||nyPYmSwI$f4Si#jIVd$KZE-6uu<)nt7mqeDk4nTxAaR)lCq|0XTC@pU%6`fDOPH~(Ye>xckbR5^iK37SBr^bLBWP?Gyb?< z6xx<|b5GDN@oVzZ9G?6CF;BC$ri`ySuNjJfEJOk)ad) z@Jsdkdq&SEu{cfq5q5R)@xGU@UY(5e&dE9RGI%-YxD5R%k{q40kGnlo?wb^;(m3J8 zt5@&t)-P~7JF6&c?&Qgv)2?dWetG#RzmNaL=}li+I)$I=biKd4+&|rKew;yl+^y?> z)r~cah*PE)~zoy zH@o#qPJTZnFffq$*SFrTt|>p)_4&1ao~~!O_e#N!#tAd7uQa}VZe8r|Z${Id8`Hba zE@}DE((K;%;q+&&meN<(N=v^R7#a%db#b=H%zW(cv18}19TwL1pX(Lv1&TCudZq2- zEY#SQt*mavv)YUU*d}5uI&G~A7e)@TO`j#tK_~q?d+S~2xzPxxd^YWy94xca8 zA81&2cUSnjn4NRNb_Ol=nywM}==1aQQiQHeSY)z{hei!8@1GHYSGhEQ^SI_$29 z)6O3HtEMh6O(*ix_j`LagoRU^xOl<-|B~A1g-)$^zIM6G zo;h=(qqCEfmv?W~(^D^l)j!Abfg-3NlC2<8`|s0C?w0rW_lMUg zPTBP3tM;_Zm$X7xiHLJNPD*xma#E4)^|%nF_3Cv!Z%gLGhk}ujGmWONQ@$OIES6%ZJCnmm6lSo1hj8&itdVqO(CJ7+Tmt8;p^5|2#78JdyMta_Jap2b%XsB zp2S^RRB~#%IJ=W?p+b+JhsT63n!i4;T(LqzLSl;c@}!h__WD}Paq9J%k%C&_4E5P$=`B*n5Cs{MZy2@ zP;mET!2|^XCJ)WHJr$|W&UGi3`_F%rlx%FgzxDwyC`20;4Ta+?|H@Z?XT0ZDX;JrQ z+qUwu6D>RnIUKv+T-_ATEfRIh>2!bSsaHu;e1E5lcuD9k4fqz9dfw<1p8(U%#y1a_ z>fZgee|fRY@r|;og*leL>KLA@%v}=RH{b5xx4pZkgM+=n$Q2xz4l|X&r*tmx5d@#^ z8L*54e2}L`Fbg;mrf4<^fI{t*h$Hw|&(yA=rF}zwaq7`f-ten3wofwJmSn73wYy8} zq*p#auk;jc%f2^%v%R*hoF#el)akyp5ti>keqLa~k{JJVMVoAorCS8cLKUtH=e)P+ z?(F-%{;`Hp=mx{$*!TarCx}E&3VEOX@IR|S?TQ(Zs?$R@7tYjSVPJ3&)G(+hL_&LCqYqm zXLCT-?{X9Prn@P(WsQfuJO7Eauyd-Sr{BC)TsY;#rXM#8nnWip()!smFDA;OLxh2W z;e~|bjr1K|S#DWx0=7P}@>&*ovb<>9jMGa3EGJse;1RReT9?+LGbJ|K`)c;hiu3)T zG5K01#~bDrt0u0OdZpDh$6NE}6|MA+z=n*g%lKnG3zbCI35IRTwA~m{IKzjHfx)4g zB~dbrtGuTkS6Bv1%UPwDpzE!!Df?(W9Kl?j(eGn`Nx5y1BJFF@u4D zK_ihRabg0ATfC8o}UMQZD}sjk1Xp?8IJSJq26+nsBUyR$Jc zG)!_6m}U|l_&L;BR(HV?DN*CykM8jWPf*fH|Fo)p+j*zs9M%)HbT@{aH!18fWME*3 z5^PcR3Hx->$~t0Yim;d7+x6;!6$f(vIrT0ZL)B6 zhTo2$ZNAF;Jo0{CWMW`&_^HIU+us)QXQvYy18Ex2wrH7BSnXBqp&wNbM!TrIJVT77(a)=n1tSOx}$ zr#%WfrbWx{XIRb-D)PK(-#9_RWqYJxOIOe1i1w#ErP zlX9kfetBqPt=|6ii+``-aEdHU+Ulj3t1?~e-Sc!F1_n@Bwsdi7Z|9eWjai%GPNvV0 zbNN1*`@em&iuFdTqvs^gc8A*>=$B+*aIjS3@NT)YYpSG$pYN5^_f*|Dol4Aa{`Z{x zHe&6g8*Z1IMEO4?voSCP^l=;#+PJXk!aB(mUdhL7%J-^gSf1P#b?^MPCpkf}HtwFs zuRs5K@py?;i;yn|14BR?#~~rb?f+&y+aY&nj$DK!;bT=U?!Kl{Fl zU{AAk=XZ-7mf707WL1)LX4EP@?Ww94W6y42=P5rdniL)TYwo2zIdkJznb;>(tBSQ8 zZsWCo^YifRv_a%VuK8|rj?2I2O!@I~{;qrOv+dXZ`af66 zD6RHhM(?yg#oP7^{&{fo)a~;(7rYO(DL!%INWZ@EaX*pU7uEB6Y^qON?GMSUJMirm zYvSjo(E5G5c5=G+N^LzV7VS5~Ao0zPLf)@m=huF_d1Ynra-W%#&TiXsf9>{vxsT3S zclXx+`~2?qzi->$)qdyWl`u$Hy>{EZwA;DId!?7Zt_lvmynKGmr=Q~U{r|l&zMk4A z!oV;^7VO~vcM2RcYQFZ_7e8e?^z4J~TSsNp$dB?|`-}X)Ow3L>9W(#?tEkLxK{r_+ zc5(Nv<8fN~wm)LK=){b*i~Fp8T{z4qZC#dg`?qU@?e90n^?QG&{eKo39$R~$k=-j! z^4_QQ)$d-eD}R6HNT;y>|EKGZOyD`3u{XFRAvyW*-SYc?-x&L^f2;E}b$Yy<0aMcB z{`x=7sgc=tzB8E=fb87+g9aS8-I9t`rqGmiuYqRz(^ zX7#sz?Fql`wRt*+)BAnA2A(_K*ZuCi5zKx30@t$4tXXQ?&-i;ykF9$7`MkZlS=Q@U zes({n6By?x(f84H6HvWUt$scc7u~zxerWZ^PrOzh1Kc7nmdRf7bgwzvC1h zus(dZ`|YgLPj{s+U|?Xd?NE4Ax>vYmUt72QmuQbopYAQ5zxUPs{Jt3zZs+}f!^duU zwfT-igx~S4t;Sz6mF<4KL6uJi5X_kxm71`SpCa< ze&KB4KFd<)Bk5D3&i$ROZ+0mEj(E-gmxm%mvro+U5~m(<*lc@tyT#PMQ;r=yw&wg5 zS7YYM@t2?b3GRM!+|Ng@GOu0U;MWn}V_*1D&L_KKZW3 zC2>dD*ZKaR(ld^pI66CR-*@Hg$seD6&^;n?g#CJYozRazydMvyR@*LLSf#;{Kieep zxNNzQ^!(aiFSlNgGp_#jrof`+%f;h!Dz8O$E!y+tQg?VKwQJU_S@FT|LCm(C znLBR&`jU9KEi^Rr=;M!1Pfu5$d{WW5tt0*E=bx(H(>mN=zxvhc`qI_#_|5+r**kh&ls=v@K7Xh3_>ves`>HQ5KI`vDn4P;#bfZ?> z!!G;Sf4^R@7r(biPlT&;{+FMh!{_luMMrDfwMw`J2Ty)+d)BO3?Q(TT=I8u*$lrhd zc~Q5RZqhsf28J)XEQ#wU-~M}Zvf~V^2Uni-|9g-he&^@wfQ3z;X1^D+f9kdS|A(vb zzEk4fEL`22fA4#~Rp{Tz?Q`e;y!Ua^qHVW>TY|rAeC_dN%jmo$8 z&O1>QxNsx;`k4LN45cD1LEq&~f6W(RHMxQiWghL<4UhCo1Le&?S94IuiW~39CGdyJof$j`@493r~U7n zEB{@Yui+oKc~Q~P*Y^vLzinpcwR(7D^06Mt$NtslW*FWrjQN-{pf|iFcjPyUw%C&F@3}kr{uYuF5{2aaer`pZ)5?{1TPF(vp9U*7HhZ}{}Ie(=gGuXe{4KAp-gTe9KCj+f77zuW(QUn4VnT;bEu=-jPSPuLeO ze{7MYJk`rhDX`FZ^4T{#f4@t5ymI+`Nuwj&hg@s~S9e5CdpdpI>s{q{E1$=f-`)93 zdTz<3{_~dKV@_IkZY=!&_gnV&7Yp0vs!lGQ-pTthX!n~pXJ_vJap|;@@6lu5_iaz# zWl;1{?0QV~+qmktx^^d2r~fJZu;Ab#{=bi1S?#6H+dhBOZ~t$H^ghtiQrlLAH?eQC z174j}nDVsE-X^i{)~(wnn-_&{sl300?@wjg|M@@q+vE3We>s;NC%`8$)8d&^@fp54 zVYhj^EPrh&51jDp^!(4l-m|}#KAro#Gv_m3xs27imv5}+p0YCh&{aQ2{?~&_bs5pU zTi&eK-LKJY7;@*c^~ck@bDlkk*>yNkz3+eSuLbcH>utWDK2f|~@_x+&)`tr&`aZw= z*#B4O3=!scIqP!1HdeCNf0Mpeak2T{fg=)jl`B8>#qWMN!?a@ala1oD7H|J*_rCsJ z&z=`sTjuz7Zrs}E82c;tO{4wG;^ikg*_NjjT#lR^6<70+$B?b`WK4c&Kc9VqnP&8} zDAvT!=hvNoSJ$&+rHRg}AJ6ldgYSve`}(cf_HmB&TEV!ESC{Qq%_`X@6u(c*o@I4J zvcR_e8}s~ree6GP^*U!_vC^ekvuE$0XSH?X%EEJREcr8KeP%SHEP{`2j!uB>?6XT5LLDz8@>9MkpV{~h@IT|Z{aha1WLx2s;Sb=|BR zVer4>Fn@fl&GY&72S3_>;k#4&esB4C%inLx`uQY{zN}n6|J(MzOfks<=VlltU%t3_ z{r}JLf5i5_IK=I1Y+w253Ey*dMg|5iF~=Lbt8_VHZ72LXD7o_Rxlj37pZ5!{P5Le^ zX8-lP-_q~Z8~%O$ZF+tFjbB#NG7bf=JJEkXR4Zm}$d<0C{Rux!*Z*`kdsV#e!=;R$ zfArig>0Xb?ocl|7p@yAb`su%-_8Urftnm5NQ5@V}?YhuJPb~RDdflu0_iwVttX9bR zbyaug+POMeJNw`7e*f*Z`cL!Q ztPgXy``#*4&)561(OrJ$yIrsU)Vf5>t2w0^er7kWj~+Z|NoEw$g%nB_y3#dE*DqZ{=fFaLi;Q7e?R<~X`H^!kmG*c zYu(Q7Qw$6Z`}!5$#M(-^+|Zq-|gPXnEZ0 z`@+AkzdxLBcjMdAN$zv!TYcf#xKMY7T;-=@57hOK`tQ}-e0<}-#gEQCxXqv4{d`5Q zx6AUAHSX~Sgi(_5q?9vptYwetRo zv))@}TEoyS(crhr%f7P}pO_`%QmZOIA6`|_q$Uluqv-&*&j z;O^GzcE8_j-WjtkR$$+cN2j-z#wcT&9w_N0r?(*(?9<~2_o)|u_F0T}qwblM`Q#-f(^t&RL z&za^Oyrnn(cYTLW(=^A;)8eO09t?`EAxwSD@b90+7NSSEu zeSh-XgR@+|FIX2YkyJ|k>%N=M$#BCJ!RIq(=iPty^i$SnqtbsRh8qru=T|)cAzC8a zQuejsOP&2jx7=LurT{ILh4r36nTJ^xnx^=CUZ1zIE$3RAz_jw9M~fnV_4qr#x4Ct5 zNyf!0J&xK%Tif{M4KqG0*tKidVLt1cZryDH<#nIGUf+JN>NUIkpY^c<+rCYWPyMIe zQGMiZkh|lL$CLejJ)6D$@7+JDyw==@itB6db*fhx-wR$=^YiljKTGZ|P73+{=XG3t z_4U{?GvCRek*m}ug*UFYvMohcdb4%+Z$33mGd=iK)60eBa>i*jI@^xUJ$d-NpzKrI zS?4=SIBWj7-JX6buiW8tc8*MZ1=Cn6Q2FaUm(1$KzV9VIZNW7C8v#l zuM=vS6uywT*jjGV-G>R;A2Q5?uD&Y{{qy(Fga7lU=5Ah{pAg=~dFY$@wQ1{K3#(V% zzZ7Hl|6;rRw%Qks?DBVVHXC-jJX*PY-mlkZ&9`zNVwbDQ*nD=6#J+#KUdz>f5&W;o zE>rLzG(29nRHQ|HZpEW5nfHI}E|qQB{eIu?Uh_)!>0LLhKOPZ&b|$X!Y4pT3>GLYD z_4`>kYOSd&JaLfSGgnWm_=6+6Ui`bc8znnkTm;^2I=xPo?V;T6_t(m9@_=$bDC}ir z^tEz#D3r{3G1>gp<>)%=e9l8pKmXy#yjl5v@*eM)s`oF${`_saQ0>2^cI6et{XTNl z3qvA~+Wq>Xdys$L$M@x58&;@gAMnUNk@jrH+@k3BcQ5SZ?^fp=cQZh3omWC za=ZT6snr2y|NrRUyxaF_dvx=L)F+|LZw@YA+qn2hxv9F_y?o*Sb-C}R96CAqe(B$X z*4Kp{C3x84Dz8SL)t`TUe#s-wZZX}g+7}buQ$!NPd3iWk#ha14F|lH-T@9=J6itn>IH?-T32$ADQM`V!Xb*j9%n__r|XY ztFt~%UVr9Y<(!ZmLEc{;X7^_P&;DMLKk@r5#b+OMA1{x2y8fC#)$4^P>hr>%SF@u_d^ys@{Q=ihrt)yqXsk1aF)H|_St*X>Hp8&W?gMP{?j zc0O+Q|5I9h!maJCYodO|-d5x%C`l~oG+_-F3j zdvnRTrVV%5MNBnj1>~HV*<$|Q>goQqi?jY+)!msJJgH|8X`KH?7IMyi7NC(~|i8b;-y2zMi*_nXe$C_y4kJ zyG&6>NW_ePN5$iJygp}bJoC(rjgQY+KA)qj?jw-gcl`16__CX~^Q)JZU%eVGUvQu? z>G9U}nFh z_p)SXEXnw~zxs&$jBN?Kwx2n6Xy&4=LhA7o<=@9nzcl05x)=VrH_InIescWHjUTtC zWe2*=+hh3Yk4pYa`$)s~{EHO_Yx8zEMcHy#|7^H->g)bLt3x6p^nctxuJx~W<#*k+ zD^9;F2>7(({tx&0UfN9?9yZl`R|RP4%n+>o^-T1%_lb|e>%ZzxUaTUu_p0pv%KhE@ zkL!JP5#Vx{t9&B)zdS89b!q)lvFoKzMZ*n*3QIqqHMjkDBl%KRxTNTBnfgEc{dUh@ z2r9e%vVLB)v;1CV`uy5&g8XaK1^8sGzWn*De`D?*spVTzt?=fU!8wP~~rcdFGuIzk=7rx58 z61SrrUu3!Ey{>&^zxUXB5zDy-%W4-KdHwd^;WL85cLKI-m>gXE^Wm@U@tq%{%Ral; zNu7LI@!+#L)BOGK);`?+w{d6g_o*h%E)HKFW*^G8*AajA{m8k0g0nZx6yLg7cgDi= zzf8Vdb6(A_y#439lgav9+;TKUuQ6On_YD(2yM?_^$nizU);~MS1Y1_~pPBJ%-kiL1 z{r62bU+3T3*!k`Er?sm*)4LmYHWqw-Y5r{6-FPh>`xScg*6Plau6fT{d+++`yquX# zp|hq*#qVFQH}7%CSMlTx>^&Jf>#r)r{AF&7+a3PD4Ai4Mn||k&e63bwTEeONqbpqA z-Eux=adGN$j$e)u^(TX`omk7IKS}s7U(KJUNm)kjIa6;wSE+x_TXAB=mARloviwfw z^2h!5b&rnBy?t+g%E?LfcR!y$E?=|{&;?q~Ji@0QQ^|MzF<^u6<=11D9#d-(LuWPe-TSAIEjDh_d8UcSBM`_%B* z+Jxzj8az@aKTa&RSriieUc~?Buh%QD$JhV;l6m<^+3UZ@Wy}9$E}wsI*YnNi?W}Wd zs72@R^);Hg=gTDTKb=Q()qO0U%}D-!uljw#jonu++)SVQ^WX2+cV00tFzjtmc%!>i zqD9Wt_wE17S0?Vy+dJ>M7QdR5lK(x4y2o|@)5@01tMu=_G5=Kd`9i0Bdzso-YD+6; z*1JF2(QW*D?xR^OnJJ)B%z1J;I`s?1uMsDHNzcH=u_9l&*>FqsTjZ66~ z&)!O@IeoKdHJ?wk$>vY%?stTLcz@zeS;?RD>)Y2IP^>)O_YX82^Rm{@N475Sb!NNv zyu9r0wBO;3x#BI4n3wTeT;2by_dny_muLIq3z{cBnYjO(T-4M_Ia7)dTbuQ%+62JSKD5kIWgIsHGJ={*HNz)D&(l1D){r`B92)9QzB}CuQZ`HU7L;ei`4d)^Oij zr$V1SDT$f?IrZpW@s+&X(eF*#^JiK;DJj0=vV76G`hySJ&zFm}tP|__|44tQ?)MMs zzm_=JvD#IK##$&>{&~N|)OSi}W9KF>*{WrhS+B3Mi&ec|_`v=A`a6~qxmMYeOmn#3 zH{5U%bDOtI^VgU7*hsORv+q9TSiUL#^yST(wZGJVt?_p;@LtWj@JGW*`G4nTdwtc< zdS%tB@Wsz%%WvLnoA-Gix0;Gy{nh&D{h`jQY2|iT_Ah@bqS=&l##&;BsV^4W~!bNe4Sg@!&qJG=b<-~V|z88QY13w9LW z*pT?>qUYqi-M{7kznI#{$iR@ZhU3t^OO*oOF5;eZ8>!_i0L`SAtbLTq4&%sMG zJq^znF((#J{&?-e**UWgCPn+1t~@Qi;`H;^o9(Bs{j+MmvzdFYa?^!%FVgvCG`qZ` zzpb5=aphU<=0|_FPp!6ExMJ(ciu{S?f|-3w^?qK<*v#f^?YHHW)V`&r2`$2_CmMU! z-n;&5j=qb}tiyX--?&V9wb^cR@ym5~)o%r|Z&c(bG`n?P6Z*QsOy?8ZvB;k-+NQ5@z9GfJylraLF`yL{eS3kK64 zC%Y$IHqtT_dcRw0^+vYdNhz+eX8tToIGncDU9F22*KRV?EF1HuSMto znhsiJ=wPnIQSH6lamHF%)u)rZ^i+5AId80jaub+eqGtZDU&!sGyGo| znl@}Nk!tyQgTb9!<7L{-bHXhz(&_|OwF$4jJWD(?ZPi~s&=LR#OGS=q@8^yZt2Qkz z_1biB_c|WOj(4FPPVW4c8*d%B6xo%s{eB~;Yo(#il9>O}gyX3G>z}6%2I;L$o5kT& zve`-C*265%t4+Q4OFvw86<}gum?8|ZdflQ3Cfp63NnlIS*Uf?_CB5-P*ukfPZBG={wnCvcEAQ->qnGo1{GnGMs#C^H@e~xa( zv0o>*EnPBOHUF(?owfV+397DzheFq_k@(o!mAw7`#aC}zUotWKk_fKipU$~_JR?OpgqkES{?}gu4ty*Uz>{U0$7;ZLE zyw-1StC1C8vT$C+y41(ob#p+=EE+a}n%t%=3m3ood8%>J>#FW6)$DzrtgWs^CRQ0- zi$80U`BLrZMvzj_1oHYSQ6)S?<*Lrxpo; z*LBD{-sraCcFLTpHbvF_>e-C?kJ)=CWu?p$$aV~qQGQZU+^lK4(`V!D*p1U3yD~B` zoRW3CvHM!hl@_c2+v+V0URd(9 zPvK2$HjmTE`6^RVZgPqA$%clg&JHo+JZUY=^CBRT@mD}Ahm-33YnOSu=6>%pR|AFR z2Cb$I<)XqZ+Id==lr_tCoZ<>Qu}-^sy4=~d7OFfSPOe+E`(Q_MiwonclTTO8{8U#p z=jwXRTamo$zJb=Pn8mRquGh|+bm6zs@A{~y3tVBl4z9?VxoPIq&zXKYI@OQToy$xQ z^SZ>?D%6QgiSasn%+&h1?F}Py4MSTF28M<*jze+V%O5a+SFVl2G*H_tVwaPPX7iiIqUcJXb zefy7#j0_AK$}EZNU+Zv$?_XUj-1R>vvb1-?;&$24=^|HWt4+FkC?<(rzrDBXb;qU8 zVxlE7Hw{y@_5X!_)^yF1ObiSO$!-GQb|s0;�ci>6UV9@evpHD#sN& zlGyb*rsm11d38ry2wdTJ%J++y7RGH~el$lJ+c=5PtQO4CwF0U&J3=9lm?gG>LzFPm7tfNyL zyL3Uwo5p3JRV2nNiJpFKDSQ2l z`oU&Nvm|C}rOhii+rLn!^j-zTHLe2FbY=;>ZUCt`5T(H39r@8W;*H!5kF_h`t#D*u zU?^!(&?#JP8W(ucU23MBuBQh}MO~+gI)3xGx9s(v<|IGz;VY#p_%Ic z>PlnHd16DaB+W~*-j*7pN%czw^<#kTAAqs~XAYYt8R^Yie%e?A~9%s83~7C%azo2Rj9 z&e^A%cdnUfo_k!|>0*g)dzaX=zfTU?2c6bg^h0asy=AZEy;iO2s+j3vA!EQ(xV)lD zZ~p7M=RuoK5}Xw{%-I$_D!F?hFZbGhUkP$XP6A8feKoFg7kCeDS>s|f^ZfJN+kv-YY?hUV zZMY#mCF;w5*|l)v#rF}fam=Cd#`Ff3>nY^j~JuJU;2vP&;3 zj5M9j&KA{SdB|2}ecirii$SQy$)Z$O9T~2h+vc84HNRjDT7~t3ktI=H`Sd0aN8hVb zQqsmZ=UlTgzM*`YZJkBa6*1-&>sATn&pZ3{NUiOMYaq|;l6AZ>z2g-(YxAS?%br!J z%@(zN_B_GA?_hMtq5_VUO4_Acw?6uNd1i?}c)hGcxgy8!NezMqJd1Z)&huF=vh-=v zB3t{OLzlj-OTGOtVZ)|Phc>Oe%^C+B6mDFR@lU@rYt_%^yx+bph?->}!*w*t@b%i(7@({3B@s25|$N+&diI8NUHteyW(|KZfU9A{Ovibx4VObFM=g; zrFKl1iNZd;=xsh>RS&g{vQ?Tcl-}v;iZBp3k+yl=k%fyvXG`CA5SX^>kdkm!aB;BS z{P57N;p>zxo7SJL+IVsOx^qUpVr$qL7#MzWHEmd;*(Jo)Hfd#u(acLGrM1u4`0tpl z(b~4AqPXvN58t=doh$PGy{>$=y?gV$D4}h?UcUMBXj!SvgUj*Vhc0}5wQ5y~nr_~@ z&laGCMPDSEHY{OnK3H?F=eV#EYiMb#pRA0%NyV#u2R%;M798KOZuca;(_JgPB8Bvy z`Mx>1$^78+9m~GG?76e(=+2XyKb7y>s+aB2aeagA?p>RV1Pp>Q!_+|D{>9L=p(Jya zT2sNvOR0UYvtI3*V^wi_{4gNdduE>q<*!<{aC!V;2tt14F~Pe6FbfI*H7-y4hfx@uSciu<@q9fdH3OovEe~EH%(pZGpVyfgPWxUp(8U z<+W7rb=Mltdo2sJx<3?$=11?D^5gvNg1xgJ>HfJL$6_@p+d6Ndc&c|%ao^O!S<_}d zzdO0^@#>t3%mV8(ZkD;4i1Dx)-8{3a4zwDP!M0IB$92(trh^GqQ@zrSR;?2Ie?+@J zqV&_gSwdIuPB52z5gQ!xeVzPc-LOE5xR9Wjl^3S3z0NrG?_)RF@UoQK+)mfG9`*Iy zbYy?ulnwi5q}Bbubxeu)ia<;^=(a zADe5PmcNshF*`KDdDUX?by}LM7OmQ(J&I)%qa=a;tzu4mv5)OdBlH1_Mx=dYIg zo|Y^6pcb7oYx3JeKU=qzv#b}{k~haUKY(>wW{dnp2s&{|Ih^$ z8XF!8wk*{U;ptrOJzf9$m89!(HW|M!tli%u zJX+-OyWJsw-o0V$O06!gzPEAlwiInYrT-V7-_(}(t+iUd;`I@WK4m*66`5xypibR_ zDS|Dk?w7YDfXb=!&x>8<^mqNucze=2YwCqq7Zrb#pf0^dGEoXNe2)*80^JwJ{ZGHW__48*f=3g<%dTwes&x!sUVUwmFIP+g~rQh`%9`F7rXJ3#=zSjOwKQ7|Gx$Y!; z<2UOpu3x_J_0}z^*vrrKW474c4+2#;x=kCNyqL8vE6PT0d+zQNk6rWa7e9Mo%WJyG zV-ee4VfI+}V%6}S55FB=3Yp|<& z26A6+T(_w6|CH7Fai6}iuUs+F`u&}U^L-}%|2)67`i<1j$F0pNCqJ&&{q>K%Gf-Lk z*Z0qT;-da#m)d4?-7386%ckN!Q8c(O0Oc3 zA8O9-eG+CV7aR&dZ(KhAbjqGTtJki(SG;}h;)@=yZ5bFC4*XK)@b)v1IFtJMWzMn_ zm0RWCNbGw1hD&pqM%$yg78?IT|2|did7U5q=jW9t@AYMNeBH6)X~g{ci8sn-XPtce zacGh- zpSw_}i(dE}&X)W4H7#D=(Xw21B5EO9tZR{JksWaE|_56W7u!^S6rA0ETSd+FvX_Q#`{i}7Pi*^hV)6ZX-}Beb2;8|& znuq1^!wXhx+-BUYng8S9^O?=Zx9{EjYhkyNyzRCsfv8gDS$w}^%erf$zh9YFA6@)D zGVY3J_KcZ1yob8P^8{``yPz8#mw2-0)$xB4GyYDjFTR)`zx{O^`|^E()$eB}{eSc9 z_mi=SL$Hh(I*@JarU;`U9}>3Q$Int$^Dm%eZ*d*7|x5B}ezmuggM&Y9et z>3QC}Puu#^WB+sZ&*v@TOBH=D#=&wV{p8cli=wZ+vYsHlHMyvN{r3)1@=w3YI1rk1)FAcl7d@$09ZROn zuSxoM)0KVUy4O=hMR&&J<*wI$ETw22HvMMN|7VBm)}Nmr^zoKT?n|AmnG+}ZZsR_5 z<;dgNeS5B4W!`B1j;HqHqMWCjlfV1C`RyIf&9c98^Q(*YE9UB}DQ@L-x_QR+|L^k` z!q@NIG-+l*!l&xxUh9HC&i^0&@z>j+*$QiR{}vB>y5P-^y^q-EKX+NB9k)mG%hT?x zq@6()+c#8~O11R%&iDDZY30gDmWO4xb4*JIXI^Oon~ z_j1mAb#=Wt8k^SczPaFPh@z@enUzIyvZQWd@^aRqr|@#A735+r%-jNzU1Y9+bQx} z7f)ItUvtwBly;7vU%GYwik?J?zpt`fubY}4?~Zrcuvwwz_1y3KXYXI|bZY6}AB*|> z)rI`+{|8Of=VoAF*a9j=JS--8s1@&wnV9WYQ$PK}lFzF}VyDT(o?>5H;o{YLHzuLd zs`j$6eEm)fT{#IO4U?3B)`d6L>7;%-_h{+K)^6SKC-ZiHI^z^>_W#rFiI3e{O^)V8 z3dX*P<&e0(+VLd!q2K9q!tc1I9{Jqsus}&l)N#kQ_MTtYBwEUz9&nw$NBdpR-a7ZE zpRaGXGu-=Ckyq(+%=TydHaiE$W!n7m-?_1ROZCTDzt0P=za!Ohwf~>pr~U0w^1bo} zWq%G7A365GyDT86_qAlpVmH3|JN|C_%G*2Nf99@~%#E+hO2k_HZND%2@iF7>+ar(r zPR)L{*xxSt$!Sp6IajbnRb7T_`K6aRX1m24D;%e;(pvY#SLoKs6|*^aOjBAtN#w-8 z@~C&`|NnEEbmfq%oAurkr{t`^{=U0DEA_h08m;%D3!)?T+^g?;9)G;v+5692F7eF&BHQMIGs_mM zz1tA$oP7C-{xY42Ejv#1-;Wg77G3tJCAr^3w;MEZa9|fG<7-F=na(&Vw)@&0#oaCW z_WPqkpVn;7y_vT`>D80clh;I#PBZc5O3QWL`S#t-lQjo6UCmEZc~p4&<1@vnzU|>I zi_20k{5!bsuRG7zb)Ju7E8O%-5*{9KTN=>*bIH?Zs(XV(1An|=EH~co2dn~+Oy6W+% zdoLUBZPT${_v`DIisVB={BBOK(v)2UtnTG3*m7!;$i0gT*P8ciPp!iMgA*47jvwhw@c~Q z{pxEE7mGH7V#fiF`&nM32{qiTzWN*+>wpcSiOXJz2$|5^u zC7r!G`QBHjGV2^a`^&w-tM2Ddb^dLUjfa+NG}dTMiHeCg^%CT-%D%xbk>|3gGn2d2 zbS?jyuKL+OkGgKYH*5E^o8E#gSFc{(mOAsAmEOed#}q`&FAMW!S^bkz4-x$L;p2xV z>8ZER^xQVy`dd?p&pCci#MduVz0~eqjb6jyw6XrlVRz@?xI$6W&0oJ?zAdj8{qvN% zUVq(#xe+(?IEojSW!bz6%D!Nc{BHTi>uQqR`@?37t-YQ8h6ywm(jey`FipnyGLzc^ zOF8{dkLGNjXZGrcaoeuG**~5{=RUb`;e%qH*1Y+ap_A^m{oHww{oSnj_NQ~RraYPY z_W4>f#qteu3q@9&wDsitNGRK3w9aFlUC|zywS_s>AMdG)-sYLudteLytNOBwS6+V2 zmYea|;_vUTr=J;3+p4E7;B}OB;e;E%xBOsC-{}@{QcdM{kh>q zSARw|U-Jq@9;gJ zuHTR@NAp@}sPn-9rHN;rb?sVr#?aa&e@_;7vd7e&1%*|Cd$P92rg(PN|H?WrReIl9 z9;=%Bf9^>7UH){<^hLB$z%=gH7sbuOZB)Xqvb5#z`m#SbNi;KjTa)!W{oP(kr+9q* z+uHbrcH36{eezi@Vw$d6Nb1gY%hV>HU2|JBb;&h_IjT(`jA}2M{6F_AIPYvu=?25! zkN?$M-`ns0B(i3|anSBR9$V&=-`?1}bN*GEnu*(L-p#t71nWnwQrA1*cUyV)PrHAA z8(+Q;v#svk_aXcLvq#YV-TEz5R!u_Wq9G6$@kpT2$R#7H~M7-Zbm+ zM^mrw`d30&n-4j=x%UtcluU`Ejq&hq1v#`}87VS9&t#iD~_s;oy z$~@G>^YYW0w7d5}gPku*SrRpc99L+Z+Vra`)z|UI$JFLF8N0HJJ$okS%5A$mZIa-( z&wgQ|+81X;?@=+T+LE32X#Kv0?Ry>xx2_afr}$XjNNcz5a?!5!W#@X|e6{uDeKt*J zx`?qw!IB%+`Dr|NC!D$Rr$0@7R2N*)9XIDTC)|PYM=XN%@aPQW5@lo=AL0rTvtwYDc-!fOn-S+x7dkQ z_MRHz|6;!iE$hoUzG{*6`vof;J&oqvv5Dk7?Y8#+1NZsoO3wW0iw?EyJ!$^$<5AB4 zE8bUyEK1YX*77mVU;FywB2b$MRH`m&I=CP+YGuf(*LU6cI%IY?mz-<8{eqdzyUHVo zY15htZO;@jk4akXY*pS|w<53m9iLwr%&q-zPQpW%L!o_7N_0c#8<<(t%vjCl>bxs$ z^Xz`@hmMohRPMI2Jn40E+RtD2|5yJ!{Bb4swFxWR)`f?Js06>Rc%cTGWtitEFipeR zJSDPET_|==^YYb~oXvfIe0trcn?3v6pNsz{2~L~3V9AR(fo0FVHYv8Zd-=s}GFmq| zv+9qp?XpVu1-tToKUGt?*t2Ql-yH`wBu>hzaLiD>6SG)IQYY@nZ10jwL3%Ui|315P zyUwlj{R{W+_%SF zcb*YE&Gt*!j{VPM8E;LjnJj%HZS#j2|L(CY7AkOMv3fLBsr9AfZQYQiZ=znlJS!e= z{rX_Pm2LZlsY^~p_*~v1w|gIGlX63;o4_I{t)qU%&2>qq6E?)&}LyxXoO^Iz=$ z%pW;*%a`+sl}$^Y*pYE4q>A_YA&~6>TpWicJ@Xez+jO&}>a2Li&9WAwl8U3U;e3~} z%sx(=JzeH{q2JH1)93s2>90M{yJEt&n@99?93nEjw=ey%x}%QcEZQ>UiR{He!Z04fIptT+w@sh{4IDABdX%+!49o7AWy3d#>R za9q#Pi{8}C0vgHUbG*T>u}O%Bt@!63sp~7>>ncTRCv7aTdb}exwg@x{<>0Bv@tZC5 zYEz?w(bRKKH|@2Z_RVEYo3rBWZN;Do7LY~Y36`D)!3!q2z1~;98#rH2-ZxC>r^(8o5lmsuEd`{yu1s&wmpd{E*+o^MU*D~>S zcPG96`kC{utD4U3Q%@~|zy?g_IApi1;Nr!GWvf2A9D1{tRW2k{)Kr5_`O=lr$WqYS zf)^kqp4Uqb8@TkO8rkmU+OtUGVSz=0#J1dY@Aq8hpq$y|F7PdM)-u+fh83GatnMZU zx2{^{c1Y0o`lp|JQa3VyhPEzj7i_7WCBq>xYt_tYGyRt8Ry(g{)BsJ69X2?$a6)FJ z>W(iUKR1**2z<+3_e84dz=hJ&n>O8C^CPSLyWo;VY|L$fT&*Imf~GUlH-aXn6OtVT z@_1ZUNxC_NiX1OAn(3oA|NS&q%_G??&5aE&e0^D#=iYwQz4afsE49EyphcQLa++!A z)l%o2CwrGN?rqyFyZguLb0=7p^iAJ0Tx>{u@o49x&>5nj2sHCzd3fxYwpC=Hsjti2 z8{dj9bUWSNw#P!`PSdry(Oeg9$9|o!l-I@XbGd}u|LwGsZlGXL?NGRLqVtsS%;g$! zFFZGW?^h5Fy6}3BXyJ<;eNVjNY{X7oES0SXhnTcs(*_=qN&bN@VqC2%Q&J8aaD^`r ze5=0GkMZ?s&=5gtn}Sa8#tfawXFJs1yln&-|H70d5qgG+KnwJA5(j~4&|^y+BbI?z z&oIzi(L*=CX`-$ZQ&0Xrtlk|xNyY4rSJ%NX?pWdKmEMuW6DZ_^3|gAqJNy%J;U2&Wm0nY0`KAv^H+GD ze(iK|QrY?%8BnlEi#YC>_#y45NoXR2;b2+)P zOG#T-Q=?R}F*d?5QbTmp1wnmb&UO~(>%}|gq!#CbM*b6$9R$85GAb{4U8ObGPc3*^ z?#kB6gV$bnlySe8GG15yH}H1h5p8$nhZ!nLO&3gj|DOQqVyG2&yrJG{#?9)y=#8VSt8M_a)F+uETscfp3OgI_%Ag7t>C^dYyawV#w0! ze!;i1?zc>P+flw|)e4T*rkgp}p1p$1;<^ZYQ=PS}wP|6A)n$?VX-f`$O*XrcxG466 zzC+f{veML43z<{THtB?bLi&p8vYJvnPdD8>_AKrHvP+LOZ=AB8zhOe_)QvMT zocd%zCBrYJrVaT5Z^gMZlJCIl8WAWPFO}RLEZ+CoR<-`Eh@)-;tO9ZdQ5H+g)Z> z->DF}=e0|I1QdJJ4k_>2xNS?3(jqODrH;p(etcT?*_ zBkx6=sV`qYaT3cfr!SQyZ`Ym)|G#hDkHazNZ|@7NFzLRa?GB-Qu5iWt=ZYduD|!3KQg88+0XYS{_|%oF$+{%I7iFR^R3Lsl^MD>9yK~G zO_|en@vYf=xua)Yw&Zv&lv#H|RK4$og4UY-tCh3@-uz3+K*x zxcp0F69OeNh6R-h9J9lXCY}-R+V}qJRyKagtEHwS@N&FZ`o+3 zN_6&pEAtZBwg6-YLq-wDp|m9;g|X%wO->6hT;G~9?Z1+@QJ;@d=7fsrT6-L}Bgyq%T3W9cw`%<{E?>O(#f57L-%cl8UFECaIZ5>ODtFzuU0b#L**Rrz zpWK$xwNz=EG$;czWTbH%N=qp?yzOm_p8S#SZnIs}&L}3|T$;XYxlDqTr?=Hd2?gVh zM;~7(Ty#AlnyR&Ls=+$zt#y}HbP4x)IEgMzHQ#aS$xXq7w`{6U_ck?6Xb<~ZlXNuN zozGl#Y08|fsnM(BM7>Xgy<=j`l4x9%(_Xx<>THylKG&@a3b+3){;@S(-HqS8$87qI z9*K`{n|A$sbkI@xo#WBRKNoIYc<4vPFSk_}f5bToPLHvOJ-M7Wd7<{pWz`qf>xTSK zKYFWOdv>1h$FN^1yEr%BTvI2LbPd!-XK)a97BG|bTKRfMqQti+pFh@IJQi1a^0(0Q zy|-08^VDt!cS$I^C`~Mgz5aRC-(S{XzctKdbi7eg!tzQ%N6cGv z{VC~t)dd%?zEwHAGP_{1P;=1cZHHSHt~gl2e(hP`s#~@#v->QxmabS7Te>y{@J#cS_ z{gNqmcE-m&0nHzNX!ol36@7ZWf6d;%=bUQ3FIZ>dwSG_1lO>+-j;)V1*#GCi;icAj z`X?%uOn7UnSbS2vjW3^fib9BiYN&|ooyD7$xa>3db|^^s`eJ6c@`QBvc6k=1E|2}w zq>hX=oqZJ)L*|AB(>|K3mzV->ja$aoV{@JAJ1o-*w}>KDXvT zo$_~ycQ?xZO5Xn-Uz2)n_4@Pc*I&4N@0Qiu7b|}zecY(N{B`}Tx4++kqSfGr0>|vW z+}nW~r=}L!%-r0lU{j&9``w*0A8chOCG-SKy|?w~lw7Y=e7yGV{#^?l%w}z|EMob4 z%-rg`)cjRvHB_5c%>P&NRJlKF5%1xOU6yKZ=I;Bo$xrlTgM z{qMt+izk)~IBnaSmVH~u%w)xnuzM}ZyGqx3KHT@G|4DGY>8@!Tyy~8_FATGKbok&ZoKx$hk( zYx(W3e|k%O+4P)Xxx$F7(&@9Tm-}e`K6cc(&sr^(|B$=<`S@>tO@E!drueK*_{1)~ z;&XQv)&1qymp87I5uUg}FxY9;grIn~ryu%$i`D;J=AsLj*`2u6BTetlx zpL70ndiYa3Zq4)gnX{_EonIoUm3ue#{h=3n`xlNGl31j~!H+D1PO5t#tmG4}QTjP41;1GU!n3bX0c!`{McO|9^Yxk1Tw)U0>Gz z`_xNM?PDgczu>*kuH;qX%3mhuZWjMJU3cN?-gQ;q)UE7G(moxHe03{(x^A4J{;!x< zM;2R!Xn59sz1XqB;bd09@%!TO6~)PpD?;|1S&{tP@xt}!yNj;bHhpZb6z*Fo^UH2= zcIW3y_7#b}#@_>rzwX)2>2&OHqVSi4fyX0`^49Lz`bG8mR&}Y&L-!Qk{5ZSQWW|qa zdF7zoGuk)S+a>DHTq(SzF5>5f@268%Z_L|h{5&!LzVMg#rySo0mK~Z~x_)cLvaK0y zdw;ar)gCx2X`J=sVlaPo?Y~1e*Y69{3xDO$Cx2@t_lDKh-*4<+&hB_Yy5;=Y8CNHH zF2Ah#SO4bZ)9YWq<6}wz&2lq*;ceRRa!HAR7}u`FQu2y>ei=({z8kn~&0DR+qn~{& z7pqm4CT_efJX!75+g!H9ly`Ho_9>|QdZk@jW5=|NW3I9KtEh0>XEQ{UPw~Gz^|a_? z(*GHnx|f1=M3mx+LWD$DP6&v<`|Z1G{Em$aH5Yqn9^WQ>GUdVV+Su@AyWVrQ2Je&U zo3Z8NJbxX`j(WFyk-vOF@uW_d z>}+qlc%5*qy`r{0><}2$zoPd-_(Pu=ByQ^1m2 zhXsZAdcN22wZ0y)S>fZIwa0d;ep@7Ivp;LH!k6PdFMl2Rdg<(q&-04nYZ`t{ebxB> zle&zv^ZB`j>EGtbfA9OWckMj|6{AgJdmrrkwBXS)^N+Fze>it`SgrfN#Q1yQra*yL z>3=o`m#m-G|7%9kq|TT1#c|b#q708m?B3unzoBquN!9<_?>CF@+?c7q_tPcq;uCGv z9KkD7!pqNV7ss7_%B?%|%foh`!r1h8+3W4CldtjZ0v#sEa6=aqCRS75oqYOBq-Fn~ zm}#l9N;0CBm&`*`j+qNbH63%5m=MysgID*fCX=6SxQNfB;?${=IyB{kj1$T_XUyX| zeEVJ8zoYw2N7F`gLjAy(DLWsP6P_e_uK+uAW(EbgD0+ z`r%ro%b9w$nJ>T#d#r;fnI-!A+& zyiD_o~ZTlRZ;?Rwu{laT= zC+=I@V&Jp%jgjX!0qq5cc7J`(z3+?p@`5D>pk&leg7if{k^XkwB`PX%I8zry^qfm?y)PFJULzNN9)eCxO{=Mcfadrvaf&l zH}m}O{lRv>l7AhPi@({ewYAsr#yv6hJD)^Xr|tVuG(W8VX7%~PV18>Cub)>opVyDr zQStX?mSe=%J;&$GiCei&{=bZI){?qQpa0Iz-@p3fXV9nzg968FzfJp2t$1&~gY)4p z&BZT8ID?EfgjzbT;uMX%(%X}BG(-P%$vZhKpzb+un}acMN~p^OtZLgVW_>~9vG@4x*2SLl20 zy?guPzQ2_I(!9;S_GInLe?Qjk|Lb?_$rIWAme*(g&d3+o_Wxn=yYBDb|7|IGbG$gc z&f8w%TeEO)Y4xfgNUdL1p6 zEPgvdbMo~yvEN^1fBv#RM|u8U)yZ}bcO(a$d?I{#e&nv4qsNN#?w83sf#whzxfj)(xD-0;qx7!! z4M{6jc^0jl+U=RRR?6Ju@Q#2q*PX6;VIEtnUjOypSF-u`iO`qb{;I*tB;PylXl@Fa zd9Lfd?Q)&#b#FTr_uI|B@_(1a`TeiEk50Dp{_FLdSNuwN>&-XOIUI+cOs+q5ukz-v zw=9~`)&0U>au%sVwR(bQ zyx&%SeZy|SU0?T1Y4SL~>+!NX%IBXP@6Y*p=VY>|aLe)TQyP*#vcu{>r2jjUx@Y3* zOO7*gGcU%M-uYIU{A-q**wmN*KJaU|*H3VBye)6{{M^6kb;smwcRt_0uA{BTbo$;! z%@eR%eN?&yJi}3_x+N0{jn=~PS-x1_szbxagQm7y3F0r2j6#Z zn|N~hzgtzew|mN)U%j#EqO!Y8-HU_k9^3rA)AN&Ce1GXSUC43H+mu-nFB|e+Ht0#( zC}CRm`tJO1Qik`+EIYmC{wp|N=sepqHet(?_VrI!mHTR@1U`hV5MZS6a2w%NYd6bsqAw~xoo|Jl-YPlD?`?f*zi{fw^azL%>$rRV&H z7pzBn6W&ZI_~G(us&(DpkH7jp&t_R@|Lwt_>iv>EmL1Q-tk&Dim{B-=?~9-RYLEH8 zsQ)p!Z~nE5_m;`q+kZa#Yty3AU+)XA&NBTai4?7eEz4!M2SjI-tbToJj&aS2@;)0A z!@QD@6OZriPQ9L#c{}LigcuvG?FP^5FV0{8f2!x(-`~tWe%yR)x8B2)zr0;jv^?kR zEdCpv7w@=oWtR2Me%Y6GA40yj9KPkM@_6m*uiDIkQJYep&3Wa%&foNh<`-2SNuwoE zYro{XN^U9?xc5!A`047e>ep-kM7=*yp1a4e)}Uzr9m(lzkNINjCoh>~vwhXqY57*g z^0$9QzlrB|%3C7$bM5x~A1_Y$`1N^jsdv=7J!j_r4fuOIRNP*qw5K%f|LVuv=Xcyc zShC0S{fYTSfmbBu=l^?ds{1{Ee(y{7{yUF%B=^GFoH>XV+j|6AJK@#@3kk52;b z7CNo^RZud+|C$Ibe>sa=RMMaO|&bdDCm)qPPpg3{jlwUf6fwrRg zKTql3%eZHMJL5yJL|fqP;zhUiOLLe`l$Gn5dB`jF*{7Rlw6D#-a{k5h$d$L}+1DO^ zz98_nM*SPbO{ps$shwYM5`K2`v@r4pTE0*4>OU@>YNG zE1D~N^%V{4lU%Fcw!U^}nwse9e601b+6(q;m!_WyUH|pHah}q{UCvgw?*9vFy9k%9nfa-!spx|Bla=xO7ha-P9kS zSRYT?qtrU#Zrh&ScVjZYtB6G`=Ln7Y+WPuv?dG#=>*MF!P4$SF7W!2xDnuC6uVg6W zWJxr>q?sy}R_^bwe0pO|@tqC+?`*@?zP`6Z_Uq{*a+Cf}&o4h>eM#Fm>iXii+PmCK z{;qGow%25Pz^7clBRf8x`|T(D>{gLd2D{MZSkSDGWREQUMqa5S%3etE5ufH2LUt9+nY)k zigq1k>z_ZV_Q%y_D**)7Y&6c__xpq<4g7LK6A>~TrS++Y)&32*H+0iPl9Y4XV z5g5+$I^Nh5bG&!aiU6(L)Wn(FKR@TbA^3Zz&0K>R@3r&wS3T;LjHubfTYs(lc^B6q z5!H{27KMn_n%xcd@4ryE!b~Re!(#XAQ}rVDrk#Et9q;G1Jf~FpnmBljuwgE<E2QfcU3^{TG$^{4ksxkXndd)5&z&+-RpsPsJ7eI6o6U=cw5aGc}?j_c_^%i+FDq zld7MQcRnKFf6|`~%5$$Qig`D!>wnI|FHr)eoojo3yew9eQJ?BHRXdf3ADqoh>{$|n zjd>0mtu0#mD*toCZGO8)>P`oJj8t6Rx4LJ#OSSCTa3*TM_UJ8`q`=Q&r- zESXgt_SGN~G_%W)kl-L-W_@mFp40iyUQd_ZJ9==_86T_QwPD-5cYqytz^_3;hJojB z!J7*1E`yv)D`)JAGJd&5KBUyCs}F=Ja^p*ANck)enp1rK`_i0U3%$V+a>Iz_p+k~{TW+J|eowPs-l~(gy{)JVHM#E+29eid zdB||t+HZ!wNLiw=k(cl;Hr>;oZNVFu84fVCD#%nwByY>rUU}+ik;L1kor{-c?pl{E z<+e3y>W+MHXEfn}lK@}Hw$#@Kt3y`Ld;WIILZ`cn`f^`Oi8%deTtDTOC1}w;0|SFK zQ&Yi%McYzes`YeV`0ldh@blbbrY|hkJ#GnHv5*byDK;yXhX;;XZr7PD6tptrkHQVd ziN`dy${ydO(_C!-VR5XXKj=Vs28M0cEDzruF!8y(sUyfyplaWC+2*xkbJ7m^zPPf+ zU-)iZI(YJ?A(qc^$Lxfj!ehLL7ff+We8T!~+nHBa)HjwS9SaWP{pFPMq z*R9|b#goQy$dL=QvXFs+VTdpfI%*!XNR{dp%!-`0&QY;-)kLSPmUpjSgM*@BE{Ed| z>7bS?yZZx=?M#%2vA?!AyxT6eVToL0iR~i(CxzNZf1~DhloqObN&c6B1WPc-p}W27 ztmif?OT40d;WP7JZX~J>NmGK!fm*qzOa;(`Su&{2S$nIap z;KX%+uTkNS?~4P;QzA`&6>bdCnyT$6aAa}I*B7gMS&qeL?>F1ibLANKWB08gAzwc_ zAItFeR-5W!bgV^SYF6lTMK^A+DP_hShwe6>GrPQn=bWt|OV`}jJ5i{_=s@g82W ze8H5a{f0U_=DzC@rDL4{y$~g`-x`fYvZSkwV_@~p=&n5j= ziwn}@nuAttJGeJxO6ivEufMMk(YF2!PWTK7)s6yu5!xIz43WfL?XC+NJwxL;N#iFkbC>&Q|_r*M<2NT-FGmyHt_XqgNM6bmPBy~viw`O zPYLX3P+81yK(bs$J^q4e#M-q=hs(Z-?p=7VWm)F7?c3NK1v)dr<3W?&3=Ae*jyK+v z{JXhLxGk~V-`~c6%gn3|epke1daB(GF?w1#@5mz1JU#;hGfUHko0i)a&n}29_pkl_ zGi`?EuFhZEzD^Wywzgg>;3)Ctl{PpC8rl>%X3z3-yk6-2v}k3B8PC1^|fW|A4CwuHCyY4T0cEM0~YPo;z!(|C_t+|*ud+1H>Ef+RNw>pxHqGS(XA1CKv=Q&+O>3@ZUwc*X`gO|1 zhmmF1wj9~1t~ys|shH}ry1hX@*Ar?sMe$|@eP5vDq?#@f)%VTU%onqt6(8Gd{WV|G|M!wfnzEe>1ya8rn)B-C^{KnP7k&Ib)vHtE z)Dq)$uS51~se!L@VKA{|Nu0cB$}5F8uio{szsry>ldU=LcuxOZ@tU5+GV63)Ehk%X zW}EYFs+dt&8?ow^g5uTEh|^}OkJ4vM@iJ0W`Plkp%0%VqcP9QntXz8|Ho!x&?PABn z!%X*&i-Fq-48DAhH8HQ9u>W;TK5MP#b$QCcOJb%|t_WsaS=M>YFR0sb zsYT+}jSj2oJ(td&aAlkD8h#bKP1)dBMrgp?+GS@*?$ z{Z*_FNttg)bQd6Gj5#o`ddTK2v-`*w1xus_PmlR(b zv@CH;+UBQTJNMUvBbs4@u>!~JiMco164ylT?z{G^((KcuhmmD(E!PHrd$^}pxLa6H zGdU!Dp`%8)6<2ERoCyU%^7%dck1cL2{B`x3RPXY>RV%iAS)uSzO{i%@hsOL4uMdB^ z|2?k@kGXk6%J{3=6-SADKZ(}cU&JBySqn97;0(N8SQ57O?JKj2#ogWaiULnR6!w{9 z649mIT;%uop<`?V+eN_@-TjhVquF@FW-G0FB-ax>;g!mgprhM=zs%2$UmCDgEkx%^ z3B=7849FfdrlfqJp0vTpOU{d8LR@#muM z^?Ayi&1x3j26LNY+U5K6rIx6v+Q@wmZtHok^2FzAlfk}LgF4x@{&Jy#Z{?gEOXQ9P zyR}~GPhH|6ay;?Mp9BHE99m5)!jo)Xh$!&)wE!yBFF5$M2Wt)Pd>FO ze2|`_U-kCp%;5e_JzIQp7Wp{w9`aq69WXsnrsY>-ptSCimYlQ$dh-mV0!2?%C`zq- zv~zKjk)w;sW1TfzfjxD#)G~Q_x<~KkJ5;fA*1w>zL-O@AN-kgWyv;q& zH2ggGF(1L357%7DJwN~6`^(>iW-dHd!`*9#o?ET|S5WWwEA8sv`!`FkU2TzeUiVkO`rl>f%x&C->j!pE=igs* zYuo-z8Tdyq%u|pFORrBXZpjpYQ;N~TB1L$xom$o`p4b1+P;Zz?N3(SpP75P&u`+bv(3>l zS|PUIc6|S|`r+H{Yp=ck-FVsh{C@|pW6CW?afMs7JmU6#XV*^CHI@1);&fu$wlGbX z8~007|Jd?h-@5G0lASeWkA7Ou370qDHEoUcZ>#qWlh?MMFOU1X@Za3BhA;J7hRi}YyQvwQS0t)e~rwp%{nW$ z<&ywzoX&To1GABfT+$LoId;hxM^G`@m((-c64^T0L#K{2sNRciw(gFZUGaI&)z~?vjmVjQZbSL-`w=bXT5UYQ?vwBB} z^=GbYpKhis&c5a^ba;{D)X*ZMn^AL1YW{u6KM|6B`mVj#o&xc;Vl(#Fmc`gqd}93V zdn^6xt{@esXGA$e)ij<4@p<9F>`sBq=| z(;I$D4`1;Mz0bTMezn$LW6r0i1xJ?j&d=HVG5chCer2|Rn{0CN`@e15?|1HXmwGL> z{qBdK7O%G1IF~I}ICH1GwN9q*PW@lWu>JR{PA~n|l>Ykux;;PQ7bW)?|9<#QTJOT~ z>fiG=eDu7;A9uOvzG&^QhS`g&4R{V;f2MmuuTChKzjt~(f4`aQx!lu6 zKYY)xUw@{jonLuB9bA@Q2y9Usby#Lgk#>PHypWB z`Khm3+$(L0tyQvH<>3k9zn`cdpI5bMs?U++jPk$DF_l+e|6-i_k+&_`-Ey&;^^~r@ zEf$}zTss#w-PNdT36Cfzr|#<6@8U`>`sQ07+o37jGA%VVC{zDK$@VWV^A#ivR~CJ! zZvNQLn!Id-*SQ$O_Nm1ykJsnx*W^uB2x+@pdT#xm+P-Ch3A&XNe~H%rNj&8l)t$CP z{@>Q$pX|5)aog*7!t1Wl%&pV*t=J*TH``&y!uusBO660h+T66@sQ*9nQQ`AfKX}}H z*U2ZZiJdZ|a{BK_%%FgJysPi|&##i(W^*6%S{k)#O8B~%%a?6RiU+Vi5KU-n%?4jJB>YS!@GWcYw0o$+xNZyx0Yk5#w*7YTA%XE zb@#kb+dgsirO@PAY1@__{k+G+#H6uU`0B~!uc9`K+f3g3clWcdonO_>IS<{Ld0$KR zcj4a!f7`h4M1Gtn@Nb9n?(@5%QraTC)3)CIP`Nn$CwMwFX#bsW&-Hh%w^=fOjgRHt z-}~#=$?q_Jm-YX%_(?a%8y|0fKH0aYy&!ou_o1iS;kF;IT>h$f(0Q56j`w!LV*0WF zZcO%ny8yh1t>Lb8(}tH{N?Sfif8IZR?HSK3+4* zeoDvR?A}~Z{9E9a-Jk1+v*%Y>#cU5dH!pfoOTXO1Kd+8g2MDe=isE+)n!5e&#t+@w zzZh~no~vHwA@D7aBT;wO#mcMf*Z22qy7N+edY|?5P;M)Zx!33SSRFg%QZ{+A^y;|u zx|QY}!sqYZsyeM|!u85N_VZ5Z=8x?gldgBXm+{+X$)S5%>e9t^yY;5)o9t_S@T}>> zMfG`6n{sNOMlJjKkbh2~hS>W(>A!gbL0yFd37|>flP;Sgw0`cp|3IxJzM}loBK}=l zuBLtT33SRmWl^0OxFaF4b!X0>jOzQfFBE+iRj_qqqC*`QoJ~)VXnP$sLYn zPF1CqB_3=pRTp*3_5Ktc{w@%;ZLa6i5`k}}r!ww_H3w|riT=4({p;kH^?z4AohSc& z-v*@C~zw7?acYmKK|M$Z0 zW`jLHzrEe_8{9GA5dtOO0^z4#M?e2z`(AASR^j!o^IImAbw{=oc&)8ylt|M^GC0P$ z(Q;vJ^pnXgA#6^rU2Y*+3qu5#?h{fi{Jpm}GUQmN_*BlO1a{WM8Ap`1 z@BO{qYfZj4zJJN?T>nR}`0Jiroj2`X$!&;!kkklFb3EUYttKvh7n?R3f&1d*A`*Z*Km&qsV|1bS|&AU22V%D$uJNe`Mmh|N7)VySz_%7huGQrc_t21AGXG;Xt zJ@$WI-<$vJ+>RwCZ)KE@%8AQ6DA!9jUohyomg9Z(^Qos^kE4y0IKZ_IXy{$`_NLYe zPqSQ2uLUlbNs0Zvvgcx!u=`QR7Z1a<%adIy z;V-u|Kj(}0715wY))bNHm0rz@1hm!4Li z>s8p??x^s4yW!uk_lhaUPQABLot*pZRCs$_q1DRVyoEb%e%_ZK-uH(?`#WeMLiN3A zVbL#noywot=GOnV;?27K%W&T>@A5WfwU6_j9@d}j2A;?5`|oS}?{{6z7B}#Ls|*QX zFI_gUe10@&<;R)RuFsom^Y@2{=d2?kXUgh6oZi}RdwK7}b|pD&mDk^&Ox`lDRVmJ` zEx?QCFK_pB@vHGEZxdtMwl?aNo&WkfD0I?71;(kiPYcf8+Pgo~w8HZj&)T(XmAVeC zm>>PbbknJfrw!%L?fx*oOcOePXV#2LanZ!mM_WD_%3m*hv9F^3+qu&bTQ`7bY=8ZD zVr;%!|9`My+}xU|rxT1bM7G@dcK6EVkG-v+$;^Kr=6$cXma;RvcjwW<*K0(JEAmqR z+dURT_S9j6oYUvTxq9`_Ri3?h?xp+o4LvJ$WLw^BdVS62XHDhgWWHs}{eJ!8AzinY z3Gd>mm0aS{bnLgk$KAGH2lcu7^S*FZUH`gxWkAUF1^RoJuCDv;|GxcyMuxDHuh&h> zl#b`&i&xm23rsrJ6re5l|Ky>Q^TWfMDqWS7X69V=UnciE_SeSOmFK(G?m60~uw~jA zE;m_^!>jm`Q?0DhC;$KF{jKl)rCTc=t2t^!wTUjD8(ii7XU4CoXYA`I*8MR5`@1K) z0kXt!E`#HZ5{t)uk6-X;glqBYnXJ;?wQBBi-Dd_f6}-bPp06nvto&?qR`}(<`L8X! zj@=d#^6YeP?$Y89T-&9&)FQYiu;RA^-a+ITB5m&%bzn_UdP2{y#N;3wtW7yDj{D%K4;Z`MUXz zQt!(uR-Pz{@{WJC_k6%D&;0$utJ(K>?_d1;P;BSS?_b^<=N)upe>dNA`7-;@H3G++ z*I2Qtzumn3wZ-1^S7dF!8Ymw5lAfOP9dvpHL&HH(aDWynPjPzsY3H%#50i|ApUi)jT_dBm`|B{rPA$h z(EGBcTD#7~>&*soS~)CNWzVjB@qA7Ms8InLalg?Mb3CV5(~Qb?$Y@2FF$PE+emwt@;SB-FP*8b=Kt0)U47@md@iR@Zl`V6Sw26Hmpdgg zx$eZS(@#sY`F_T4e`3C;(AB1U^41iiL8m05rp!6plm7Tqk$B zYz*OQveTbfbS>~;xc!BRCp9Mdc}>>#tN&JiTK%W+|KzvUD{uXWaEAwlrCwUF*N z=Jy8E%JSZM#OBxE{9Uy0#@EYlZ=U`=r>S&W>edvaU2MO%$bfdaGcddXwHVT(x4qj^ zv{PqW;?8q>+cxdBs#$tsW~cLgdAa>NKOT!;H}m7#sr_Gn$9nA1l|Q}Y(#K~XuAbU9 z2R#1^Neqiurd;B0O4_(1)^B^&C-M7c9HnLR_Wzi-yJ&s>>65v~J=G^44Vq}SrxrRo z=frU+Eq&&+%vno=PQL&B$|%3oqv*%-cA1)pq#qo{q4jBf_qU!nZ~sJpcjD2WTub9* zliQPe7a81W<6H+x7{-bmvt^fF*`(uEsW<<2-03YctC}yo;yv`vwN7u!?JdoF_MJ#k zT6p;CXWKQZ;AL?J0SX-K40EIV-nuwV@mi{OMz^+d-QykWqF07ZeRX?XC&XB8MGkhh zWs6VyR_t3pd&A9l7rd7SZA<;>DBwCn2Ru$}FhPNXod+~`@Zx^hSL?piSuabXyk1t$ zQ}Q*h;shHK-Jl@jQ6d~QEmUjz<6l`#I~Vums!u*?z|*{;QwlOuC)J`Lm;8 zc^^+d{j_ec#N1^sL$rh@zW=@Zn-|!a1@!_g%>2*WK#iAhtx(PKgGzdHif?a=JQ|R^8KA)?ZEMWXjP2=cf(QP6F@p9xYnjBi+_# z_1EIuK?|M?v|w~QGTz@Vv3iL>!XS_Qx^wqs)J0Zy*Vb(((GDmFyY$b2?E!Bk3_q7bR034 zTGiXm3${ODf`b5G(94iruR<4V+`1|b@x^YQrh*BOCM*NPU}kPu&~k0!E&c1I%?ceZ zE+Kzb=Y_{BL&7M8W>P;mCo@=!G!>X1v^3`puIF96B1Dh9PQY`ac7I+^jw;7%VUO37 zJ$Rd?=crHiJie_|fGtr&brZPSO4#0{AoD@u@wC*J7e3ppUsY+>y*A(Bnsh_-9?cDP z@(+I<|N5KraWvQ0@RgelCj0H~6W;G?^H>BjXSiXug8*O7wA9&cf-H{f0#>|NJZS#p zL6}fMw%IACzpj#P53Ta7ruMiz`Tei#HzbrcyjI{~H;BHzAak1b)S#6zT^-k*3Y>ZK z9xRa56_FB8@VU0V2`H-U5n!Tyu!NWP$+k(IDsad$dx%Z{anqKaAFTUU15V-J4 z{`7*oC04EAV}ThYE(o+RbE;lhqZzKXv!<~)Y=3e=dR+6GUg1|pB{{p-Zr!>qSusx& zJO?c?L7;`%vo>|Q*V0+qPdsz~&X6_tE#Xe_adq0VZHtPK-ZVLIL`p0WXkm7<3BFQT zd;ayw=dbT?nmey)@1x(G*^xIU`Mp*RC^mHoLUA7$Q=M|s^rr;QOS z!^+>*HqQ>e-+joS!aaKT`)Wb48Q>f(p&-z5R&S;Uo8yJar;mGg9sWI*Y^Cs&*m?k`B`gq)sfHLzl@KlXPXOeON%bu zlJp3?yYE0lgF;T+oWkRUyLMF{GU!q7{#IM^;m+f?<+77zZQ?H%>Y33~s5NsH?|n_7 z`h`{xOC*kIA9QhX4XV1h%CzWO%-5Cozkm6<_f>Pt*SoK@r*=)4VZj9o-3xC7TF#y} z(mi%#R_W`W9c+>FXsf8&(#>A4&pj z@$+4kZQ$ql?HIG`ibYy-0&Dzb*Y=7Y+b(#jqG!EX|C9+1t5%7qmY$w`Z!3R@*t*P; z<3DdHEPRoXe?3*Q#j)Pb5)?KJZ*({gJ#&dY{?TTAgw{))oIk1YmHFR3FPAh)-M|t& z$7tCS!SzbPze9KDPsp92e(SfNR$Q_hm#gYTt$#~f{>?kKU4*Sk}w}EtJC}Jd*&5p zMced;2rQi)pdq9cG_89&tN5Q)7R8A-PE6IfJ1Hn_bL3x(BWoZzU9C+a$7)K@^}@pj zZ%#fh6*H^&{b0M@6sz|k)75z&8Jy}gc_gP2@J<0GB^P;Z*4`BwZ2SLJzueAWrClu*mEgADYNE%E zb=_Otz*%sXWYdP1FF39^M(9k_ikoU~6JPT3t8-?TX1k#BJk3>)8XcGJ`1SFB^yf=^ z5@M&nc(dW#&Dr(8CndH>Xw|h&YT5VNc75=ooJD$n=3gwoGqE~1?&FM=GxKW_B|bm4 z=FC>^REpDBrsb;m=S^vA^}Ns0J0<>d9X8mct2^(x=hYWVOJ^_MvG9$`YO$=>i{3E> zo;J8>cPheo{`6htC&5{R;f(@l*}K%T#2G)G96MGVysK9C^X8k~{iQuB%5#FwXDJe~YU=+!xqU6?^hS5N zK#i&MDqhXh$SV074$4vu+glZox3((mdAC`1xop1C^kdc`FVs@Jj(Sht^ge#)vybVg z)WqHWawB)P?fF;r@y6oG(`!m4Lbon;)!g~$)Kc|%23i0A`nVmcI2d?7DAij;Cv@YK zE6=+;7CF6Hb;&oP*F#n2>&JC&>)bV!mb`fL;zP5;i;S0t*GcAgkiEzC+)>f#X9L2T z%Jrq|eir_coSDV{?MAqLvhLiY#j8P6iPH-1o@9SxUey1vFZcTVkG21ghCkc$>sL*v zv_dXqMzLn&j~kOO3xbZaVA#+Lnor#JRzu{eSLD@cukXD2*#0In_WRE>A1& z_a^;h5j?U%X`8G`G1q1zMr3!A+}mz(^}K$+tW5IbUeEKlbX!swb{O0 z-=Eg2%>T9e;?|bXPuBJvTQ3R9_qDyv)vYl5c!~Kd_v@bzo+?@Id3$WV+PwYAxAl{g zkINQ6IU#uX>;2nrcCN3SXI^#XM542G>5rFxqW2l~`6Dg;pX{OHwRBToBb)ggi?;>e zZ|QH_AhP)5wB}#)ryRHDpH#8sL!W(U(Dp*J86XEX8*d?eeYYEjam_BXi19R z$KSV~+x~cU%0A`;->j7qD~&v#J~BQHokhjx4uXF@H)94@80$Q zITB^0U3}huO{)Io1zxFJb}CHqb5o0Yv|C-fGVJW+V|g((#R7J^L2pkLWj^_Q=vMFl zt|?OkdV&QnT?k(G*tg34{O|qRmt}9>-Z!`6pWojl{{rvZJxD9rvVHESbHblm4}I3( z6LH@DvdMQD^Jf<{CyVRHetYsI{pf6gui8uNf3$B(ow2hb?b*R>F7?`Tx4*X({9rlc zTm1F4&Gu7s3)4UCy#4zNqj~z97lH0FWgq5*$Jf{1Yg+aE>U^17mMOOagNs|6^c?=i zyXFg=oA>+Gj*ZEa^6os}S(ErK`?J^6Ph~qn7f3Kj*a)=vs!#QK`nl4cLrO-m=6uNW zTkE$J&DK7aawJpL)h3*)IpE-(hE&^mD~`+-FcWC~UuW~{;MDbOVUJdL&W-h>n?{}(REHhO8^-WGgv3E!c8>$HRKes4LX8Gp@m2ht|hL!S<& zKe7AcJhxR^eoozs=`(+?Tacyw{w?E7O+V!=i9f<{B2V_|GDI+^WX1O*MFZhWqNNar-8(qyI&95ee+IZPAt1{ z^=;Wxzt^w$CGvV&YGT*@n#F$?PXEN)vPhxAMlLj1-&A1Mx+L3x#eTb0>ppwgRon{v zZgu~*)>fHEO0mvN3(e}v?*DzQb5wcmoz*pe#I8+RIdgkvuvlVjC|~Zk+-kn%?YB7$ z#W^OXcIe7$_F8MGuQ1)dWc72U%Nmj2V}IW8D~VG`(Y@7jQ)j)J*gfym<;nN8i$3RV zT-^EIve=V#Z|`&_J)*f?R;Cc%#vR-E?o{F`;^OGe`< zj!&6$7G3h;IisU|=ITVlS+l0<=5Jp%aYuUn4JRA7w29MeKKp%_HD}XsEsJ_`=FAs= zFXP67%ge60Un~B3NA!1_0>`(omRVN2ZmF+~EZ?w2ZLZye>d%@BA4hB}^iX&cw>OmE z=kX%jrP=Qu8ME5^hI>{V^}jl6_RfInkH6TDZfkp}XDGpObo1ALy!@XRmi>1B$XX>= zclS$=;4P_RcJ<;`_5%NYS-(+!ZS$u$-S_>CMgJ@B{CxO&amAKDcegzFb?Ow?x8ob0 ze$P)YpLc@IsqgW(H~%+mUTs}{>&&gp%~|PRzI^+eczk#G*E&#z&2Zp356hvZ$0Zhv zKUPFt{&c4}=_k|E@P0AD##Y6N%T$dYxpB5H^*Y|P@cDMRH$BTOSDl|9Zd6~k)7d1_ zk6V4sFJ)Qw*THH}jdoua&N;8X^~UK_`BQ3BBRdae-&~|~<O-L4+? zG(6;(oT7+p|LwI|vt}MI+IL>cP+-=&N9UHmIm;Zo`}*eMqIjP(zi)rIZ+raRz5V}w zdp+)Z@pko=SC!6I-}nAh-nds$oMZ3o`DO2;rd!|o|5W{v_rBBjSn2}2FReVy(<1sb zm3#gC>vt~~?^tLk)cf@34;{bra}0B<&Q5VL`g!o*u66Gcw%JN^L_S`}|83=}X}N1w zt=UoekT=Te@2%OD=l|me>30Obhu@HknzpW=J6$WOs+S+oxEYMi>|TDjGu04e;1~fZ~XW) zxPPD4^WyL|?2(h^=q+Cs`$_818bRiTc4a}|ALnmArda5=dfxrddwbvX{(tH|_3-UC zq4mbo_+IXLvOnz9@v^$0kZ0GwYrFrSVl5S4;Q#-6-I;qme_u{-S@-NTPfPInnI`q; z-W1d?*|%`x-x&9K{r1VXu4FI2enhylZSnD2x2%g=EA@7~b$Pmjfx9I$w%q|dpvR!c z35xRona&ii%U5j9?@w;r^zEWjpL*F@iSFf^H!rzlam?7fVaB`AV0Y`s`yrpJkYN5#(aiR|0<@XRXT%23Iz%nNm_ zWw^LorbnfkWFK+J5D71}%{)KhOV0oD^Va*%)CR7Nyi>h@F8^O?xj?>r|2rF;|)PCPAncdxu*>A}y>?>K>H$ru>I1y~L>DSL0qdvP*F>-^i_)AS40 z#=bS?4SxHGm+|MSxh5-a!gGtB6o;<~pMF`Ds*P z%hMmEEP2#^ePQ={#V@(P7G}Rcy|`jaqJWcHNa!`c`C8tW4j$%R?zf%Cl935Acd?^M zK|o9KT)0Yymj$OL2H(NtrOFmco-UoqSISO-%l7GCmH~jl}PPMt% z-jC;Zep(yua&^anKRd5$ZrZc|-G!CL>GsyQ#ZWvtPu#_^<5*Iq*z-Gkc9k78-y34? z{QLG3&Cqk5Ps8GuU%csO_||%L=$$g>Uw0(6uk~x3T`Qa7d4AFS9J@yL_>H#@v#Wi1 zXZuvwZ$tl_^(`!`CT4!!Q@$a^&oWSE?$^Kf>mF6aK0k3eQrFA7yL;;vvt2@qHTFB4 zkW;ETe?BNa!Y^_~cgdrYQxe9VTQ`4r_V04kOP{+O(@(G-@|gZoS9xQci^b{4BYd9A z`KIitkT-j?O4pfbq1`g8ZEt>`e7*0~y~z>%&)1r++biw-Wx=srlb=d-9SS z6Q1iHo^qsb&V$w+M*HT>{dTMP>*~07fuB1oB&PrPt7&SwtIJ43oWp;k!w-F@=O1&D z?{AOuIQwT(#jdZqV&(IOzm5gOX9{z#eaP{E$!>FyZe+*|QfKXg_0ukTm1Eqb(8FFk48`!8V8#)GwQQpW%AyQEn%S0wuN=Upx3I zZf@QF^gAi-`B|oqQja9{v~GA3&@6khzyH_tJ3e149{s&5Gk@*QCSz_+fq=Sk{lCfZ7g+;Kf|LFw$;Tz4xiabE%Fm8;gjk^Q$WqFt}u zS>))HS>b`c;8p~~fq1Yp7EL+8y|AF-NRiI&_mh{|lyrBV+`ao&ZR$G9&#O2KZ?m6a zF0N;^zIoPvzwo$d^k{Y@t~rU7?0`Pis`zFE{motuVNSc(@`8HA)Iyf zs_gssJ!fVcUwgg8_V(;${}<#QasNxx^c{lZqqZcIEMvT|ZiPIZ6zgwI}Gby<_E zRtaw`>oVEbRm7#HnZc}W^1LGZ?bKp&)T1B+d|K8m&=e$6kMplA-COnrdQ9Z zJ>X$m28IXcS(p+VC%JS6N%bCWvur>7;$Zc$x~I@t?XoVT6KJdnA$mpLrkZe zPxvU=X0|nD&s+QYS$Ey$ozdp)-Bn$CRre}Sl&M?^sS6k$=yNb7HY&|Hu}P-Wt$pRC zlT3-9+cY;mQnHm>t#Lf&WyqPG2I{vn^(&{bIxU&9*`$L&PpBj-SPN=LF0u@YaS`>Pn>CW z2C|8kVZjDR1)Z3#H4<%ty~o=%bw0N?6u+)NC$_p$$5ODnsb#s&xkSO&iPbOs3lfcc zeXd^Wy?6gz;IV?1{R!zNvdccr$)0lZX-7xLWDQ^C!v`kuD_)jM-lDv;E8-aUvHx$= zb+ha0-z9yYtgC)_QO+x)SCB~n28Mq&3Ibw!^B${ua<%@}l+}w|yKm`>d)9Lsml_Kk zPd;?Jlkc-gTO>X*3tO%9oY;5n==ptL_&yb=Uf;nHrsKq>)g9CldBr4XmXxon z$=YV0n2j@LM`Y!@Ho8vPt8{JSNuz}`e=T?^KmYxWdnR}EKHq%ks3z90%1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + '@cloudflare/workerd-darwin-64@1.20260205.0': resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20260421.1': + resolution: {integrity: sha512-DLU5ZTZ1VHeZZnj0PuVJEMHKGisfLe2XShyImP5P/PPj/m/t7CLEJmPiI7FMxvT7ynArkckJl7m+Z5x7u4Kkdw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260205.0': resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260421.1': + resolution: {integrity: sha512-Trotq3xRAkIcpC505WoxM8+kIH4JIvOJCNuRatyHcz9uF5S+ukgiVUFUlM+GIjw1uCM/Bda2St+vSniX1RZdpw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20260205.0': resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20260421.1': + resolution: {integrity: sha512-938QjUv0z+QqK6BAvgwX/lCIZ2b224ZXoXtGTbhyNVMhB+mt4Dj24cj9qca4ekNXjVM7uTKp1yOHZO97fVSacw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260205.0': resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260421.1': + resolution: {integrity: sha512-YI4+mLfwnJcKJ+iPyxzx+tp2Jy4o29BxBPSQGZxl/AZyvZ9eTKsmNZmtjEiT4i3O/M0tdO/B/d9ESDHbRCs2rQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20260205.0': resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20260421.1': + resolution: {integrity: sha512-q1SFgwlNH9lFmw74vh7EJbJtduo92Nx51mNOfd3/u6pux6AldcwRviYzKEEv3FEbtv6OBB7J8D5f8vtZj7Z6Sg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-types@4.20260205.0': resolution: {integrity: sha512-LTnpvcodmiuMwxmbrO2Fd0+Avbm2UVLLJxT8J2pRWPfoM44gmbIecXwOPZmDAMeadKWrBsQ+B0sloQAhUu5fpA==} + '@cloudflare/workers-types@4.20260422.1': + resolution: {integrity: sha512-kI4baXLJloST4J/PxAfhz3azy4TPC7L2wcX6nusOzSNJJD/L5CSsivSJobgkYaMG5yspzRFQUQlCdPza0nTHbQ==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -281,156 +471,419 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.0': resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.0': resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.0': resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.0': resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.0': resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.0': resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.0': resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.0': resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.0': resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.0': resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.0': resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.0': resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.0': resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.0': resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.0': resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.0': resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.0': resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.0': resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.0': resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.0': resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.0': resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.0': resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.0': resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.0': resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.0': resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@2.0.5': + resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fontsource-variable/geist-mono@5.2.7': + resolution: {integrity: sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA==} + + '@fontsource-variable/geist@5.2.8': + resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} + + '@foxui/core@0.9.1': + resolution: {integrity: sha512-GnMegqx5gz71U85FK85ayEEHuD3bvpNET8HeNsJU1Gj+OVkKKvUY9q/XqmCY3tsfEqY0Mv+RM1iQSnsZn5LZ6w==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/social@0.8.10': + resolution: {integrity: sha512-d9FB1BjWFbvldNlrXogdJU/a90dDdNqvBWGAkLRK2i7iiBAaWkKceWL3xc8N5+79+hfD8Ydg4ptCk3wx3bkguw==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/text@0.8.5': + resolution: {integrity: sha512-K7XsJGWdyRdXlRxjD8Z4TQ9xy7T/rVrB/z+U0YGGIjIER1DQGLawPwBQ5WXYQeNCBS1ab8mG67uFZw2sA5aFPQ==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/time@0.8.5': + resolution: {integrity: sha512-InXhgKZW4URArJWW87hblhHMAoNii0iueKCxei72SCvlABZ/y5QoX/ewhrgk7/47qbfrlgyqBzshDtF60kTqcg==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -577,9 +1030,20 @@ packages: '@types/node': optional: true + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + + '@jis3r/icons@2.7.0': + resolution: {integrity: sha512-RXee+W1AoWFHtFliLZ7SQxVopAezmCH0xU45HKTvvpDQ+6Y3g6tUHvKOK16J52TM8YsN4WDnsiOmcwau+bHmwg==} + peerDependencies: + svelte: ^5.0.0 + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -593,6 +1057,11 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@lucide/svelte@1.8.0': + resolution: {integrity: sha512-+zYQUKqEOVP5lxbGmxL1OVgGMQtRK91eIJ0bR+3Cr1ts4oQEsQfxyzzd5X47psJlblAuGFrl2xm4YuATjR9oaA==} + peerDependencies: + svelte: ^5 + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -626,6 +1095,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@number-flow/svelte@0.4.0': + resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==} + peerDependencies: + svelte: ^4 || ^5 + '@optique/core@0.6.11': resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} @@ -637,6 +1111,9 @@ packages: '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -873,61 +1350,464 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 - '@vitest/expect@4.1.0': - resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + '@sveltejs/adapter-cloudflare@7.2.8': + resolution: {integrity: sha512-bIdhY/Fi4AQmqiBdQVKnafH1h9Gw+xbCvHyUu4EouC8rJOU02zwhi14k/FDhQ0mJF1iblIu3m8UNQ8GpGIvIOQ==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + wrangler: ^4.0.0 - '@vitest/mocker@4.1.0': - resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + '@sveltejs/kit@2.57.1': + resolution: {integrity: sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==} + engines: {node: '>=18.13'} + hasBin: true peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 || ^6.0.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: - msw: + '@opentelemetry/api': optional: true - vite: + typescript: optional: true - '@vitest/pretty-format@4.1.0': - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + '@sveltejs/vite-plugin-svelte@7.0.0': + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 - '@vitest/runner@4.1.0': - resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - '@vitest/snapshot@4.1.0': - resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@vitest/spy@4.1.0': - resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.4': + resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tiptap/core@3.22.4': + resolution: {integrity: sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ==} + peerDependencies: + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-blockquote@3.22.4': + resolution: {integrity: sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-bold@3.22.4': + resolution: {integrity: sha512-jIaPKfNOQu2lhpbLDvtwlQqM+mjF+Kk+auHpzYjBnsuwUli1Cl5ZOau7RH+rru/SQvZe1DtpQlANujDywugZAA==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-bubble-menu@3.22.4': + resolution: {integrity: sha512-v4pux5Ql3THAEjaLMY4ldtdy/Xy2qU7PJLBkq8ugLp8qicaKC+tpqxp6sGif4vLIjz7Ap5hurRbTNbXzszyyHA==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-bullet-list@3.22.4': + resolution: {integrity: sha512-TB+d3fGcTixYjO7coKqTr1mGTJuqr8hjDCPUFgzuvKyJnBhqWITmBzQ/8CLq4rr6mihgGURbD3N+xkQuPAKFiw==} + peerDependencies: + '@tiptap/extension-list': 3.22.4 + + '@tiptap/extension-code-block-lowlight@3.22.4': + resolution: {integrity: sha512-Gw01buMOF3EMaQHFADXMw53vvDaUcHC8ERTjw4IlwM33CGGDRQmn5cwxUvvhImyrc2FzzE2VmB5KC11PrtfORA==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/extension-code-block': 3.22.4 + '@tiptap/pm': 3.22.4 + highlight.js: ^11 + lowlight: ^2 || ^3 + + '@tiptap/extension-code-block@3.22.4': + resolution: {integrity: sha512-MEurzNXfMET3rhjpoPJYUgMfxTdTqbzT9+ToFrqNGAHocdXVm6m1hhO2frVC7fEtHPnxXKsn0Z3NUbCRkRTLuA==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-code@3.22.4': + resolution: {integrity: sha512-cnbxmVhAcc7X3G81QUYEmKP0ve2hRmvAiFXBuuv9RUtQlBiRnzmhHoJOMgkX0CsMR7+8kMRpTfeDUYq2xp5s5w==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-document@3.22.4': + resolution: {integrity: sha512-XQKla1+703FqQJC48tPDVgt9ucGiFbIEmQdOg5L5o07z9a6/NzuaZAc+1zJ7NxcUZzy+z6wBn1PrVMTiqiSXlw==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-dropcursor@3.22.4': + resolution: {integrity: sha512-N9/yMDC35jJp0V/naL0+6gi4gUDUIcPpWEzFdCDWUSYBA8mt41c1kI1ZU7UTKYIBzTClenhYHRc2XKZxxx0+LQ==} + peerDependencies: + '@tiptap/extensions': 3.22.4 + + '@tiptap/extension-floating-menu@3.22.4': + resolution: {integrity: sha512-DFuyYxgaZPgxum5z1yvJPbfYCvDdO8geXsdyqt0qYYdiat3aGE4ncJhiLRIFDhSHBhaZg5eCgu/YPYAN6jZnrA==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-gapcursor@3.22.4': + resolution: {integrity: sha512-UYBEUj3SFpKINIE7AdzcyeS3xICK+ee+YLBbuqNXyHStYChjJOohzJehqiqhjR16A88KQQ+ZjgyDcItKGygSog==} + peerDependencies: + '@tiptap/extensions': 3.22.4 + + '@tiptap/extension-hard-break@3.22.4': + resolution: {integrity: sha512-xq+a4dE7T6VwApCkh/yU3p30gn3F8g8Arb9CyEZm58/WIJUIGvHSTjDdHmvU16+kiWSBg+wOOsaFHhYjJjxcKA==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-heading@3.22.4': + resolution: {integrity: sha512-TUaj5f0Ir5qy9HKKt2ocnwfXKpZDYeHgbbP9gshKFzdq5PLe1RbIgkjfy6bnoI865cYjmPYWRjcT7XsKyIcb9Q==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-history@3.22.4': + resolution: {integrity: sha512-yk1RrBJty4F3Os+w6zIS3nOSOKy+iIax3UyXwCKiP+JI4nsTlYvgQTgTtT9Wqa14ZrypasV8gAuQeTNf/spFLg==} + peerDependencies: + '@tiptap/extensions': 3.22.4 + + '@tiptap/extension-horizontal-rule@3.22.4': + resolution: {integrity: sha512-cCI1HekGQwhY/MbgaKQ0R/7HcH5ZM1oFAyI/J72QGLC0XnF403S/OXoHMuBWr1mCu8hNiQWCzeNRJUty0iytNw==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-image@3.22.4': + resolution: {integrity: sha512-ZDc+fLaratTQ4IgnKcJJwfUgUgpcHjbZSBi6UQAILJwkflMy1Zxj8mpbma5P934nLSI+uDnR5ret6ZZLNITKhA==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-italic@3.22.4': + resolution: {integrity: sha512-fVSDx5AYXgDI3v2zZIqb7V8EewthwM2NJ/ZCX+XaxRsqNEpnjVhgHs7UlvDqK1wj2OJ6zmUNjPtVlAFRxwT+HQ==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-link@3.22.4': + resolution: {integrity: sha512-uoP3yus02uwGPVzW2QaEPJWVIrUb/r5nKm6c8DiJv9fNSX1+gykZZMg42c6GwRFLZ/vyfWjVCbAE03VMUqafgA==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-list-item@3.22.4': + resolution: {integrity: sha512-H659KXTvggSypIDWSOJBZ37jh9pKjQriDDvYPYvOZCdfij0D0hsDXN/wXoypArneUkoBdgruHfTtMkFOaQlgkw==} + peerDependencies: + '@tiptap/extension-list': 3.22.4 + + '@tiptap/extension-list-keymap@3.22.4': + resolution: {integrity: sha512-t/zhker4oIS78AIGYDdFFfZC6zSBlszfD7z/zqFLGCg5PHNNgkZK5hKj6Vyix6D2SapRn/ajnx+8mhbKIUH5eA==} + peerDependencies: + '@tiptap/extension-list': 3.22.4 + + '@tiptap/extension-list@3.22.4': + resolution: {integrity: sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-ordered-list@3.22.4': + resolution: {integrity: sha512-w77hPVf7pcHt97vfrybg/l0t5CimCd4y75OJKuHuo3CfgM5xbUP/gaPNMDyLLe7MYole/UHi/XvG3XjgzqTzAw==} + peerDependencies: + '@tiptap/extension-list': 3.22.4 + + '@tiptap/extension-paragraph@3.22.4': + resolution: {integrity: sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-placeholder@3.22.4': + resolution: {integrity: sha512-Z3wtWL+KufwkC7CkJge5enAxx4q8C3oOYixme02snY9zfjX3V/1pjAmEfP4wxScgM5GIuTEJ83B9Yz3wRzPA6Q==} + peerDependencies: + '@tiptap/extensions': 3.22.4 + + '@tiptap/extension-strike@3.22.4': + resolution: {integrity: sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-text@3.22.4': + resolution: {integrity: sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-typography@3.22.4': + resolution: {integrity: sha512-LXcJbhLl7m8jQ+VFj8u6T5p8ylZLv8WUUZQal7JF4KgDLBhPGiS0O2HXXzcRfjuVzT+erqakwybnS+0bkTGcsQ==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extension-underline@3.22.4': + resolution: {integrity: sha512-08kGdbhIrA6h10GWXqOkqIveaBj5tmxclK208/nUIAlonI9hPd739vu7fmVtpnmqCnSSNpoRtU4u6Gj5at0ZpA==} + peerDependencies: + '@tiptap/core': 3.22.4 + + '@tiptap/extensions@3.22.4': + resolution: {integrity: sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/markdown@3.22.4': + resolution: {integrity: sha512-gcoLOq5TBntw13QdeWMy7yc2X+b9yTplcFb5kpMQNgNoxlu0+jlX4LiBR+E6lOV+m+AtkprHHltTYyKG23bdOw==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tiptap/pm@3.22.4': + resolution: {integrity: sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w==} + + '@tiptap/starter-kit@3.22.4': + resolution: {integrity: sha512-qWjw+vfdin1rzMRpRU4cC5tLTwMJtUpXeQukv+6mOqqvhptuwuZBjUHImVEJaSPoHXS7+1ut+nTnrLyWyEuE5Q==} + + '@tiptap/suggestion@3.22.4': + resolution: {integrity: sha512-1buvLZemITTeKmPf2wGFWvvhRFKjdQ+JgMqc67xBraOKeDd8wQi1e2XlhCYAtlVMm5f6j+qlLC/MvwuHI2jHeQ==} + peerDependencies: + '@tiptap/core': 3.22.4 + '@tiptap/pm': 3.22.4 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/vanilla@10.3.1': + resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==} + + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} '@vitest/utils@4.1.0': resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -945,6 +1825,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -953,13 +1837,32 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bits-ui@2.18.0: + resolution: {integrity: sha512-GLOBZRVy3hxNHIQ2MpD/+5aK9KcBFZRhUJtZ1UDABXdlVR4K6zFpgt4T+Rwuhf2sQzlc6yK1q/DprHPjwT4Pjw==} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -985,6 +1888,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -999,14 +1906,29 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + custom-event-polyfill@1.0.7: + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1016,6 +1938,17 @@ packages: supports-color: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1024,10 +1957,26 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devalue@5.7.1: + resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dompurify@3.4.1: + resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} + + emoji-picker-element@1.29.1: + resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1043,17 +1992,104 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-svelte@3.17.1: + resolution: {integrity: sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrap@2.2.5: + resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + event-target-polyfill@0.0.4: resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} @@ -1064,10 +2100,19 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1080,6 +2125,10 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1088,9 +2137,20 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1111,6 +2171,18 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} + engines: {node: '>=18'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1118,6 +2190,13 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hls.js@1.6.16: + resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hono@4.12.8: resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} engines: {node: '>=16.9.0'} @@ -1134,6 +2213,20 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-emoji-supported@0.0.5: + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1146,6 +2239,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -1173,13 +2269,32 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1250,6 +2365,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1257,20 +2376,45 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loadjs@4.3.0: + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1279,18 +2423,40 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + miniflare@4.20260205.0: resolution: {integrity: sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==} engines: {node: '>=18.0.0'} hasBin: true + miniflare@4.20260421.0: + resolution: {integrity: sha512-7ZkNQ7brgQ2hh5ha9iQCDUjxBkLvuiG2VdDns9esRL8O8lXg+MoP6E0dO1rtp+ZY2I+vV1tPWr6td5IojkewLw==} + engines: {node: '>=18.0.0'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mode-watcher@1.1.0: + resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} + peerDependencies: + svelte: ^5.27.0 + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1307,6 +2473,12 @@ packages: engines: {node: ^18 || >=20} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + number-flow@0.6.0: + resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1314,6 +2486,13 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1325,10 +2504,18 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -1421,6 +2608,21 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + plyr@3.8.4: + resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -1439,6 +2641,22 @@ packages: yaml: optional: true + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -1459,15 +2677,120 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.5.1: + resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} - engines: {node: '>=14'} - hasBin: true + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -1475,6 +2798,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rangetouch@2.0.1: + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -1483,6 +2809,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1504,9 +2834,40 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + runed@0.23.4: + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.25.0: + resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1515,6 +2876,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1534,6 +2898,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1570,6 +2938,9 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1579,6 +2950,77 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + svelte-check@4.4.6: + resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.6.0: + resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-sonner@1.1.0: + resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==} + peerDependencies: + svelte: ^5.0.0 + + svelte-tiptap@3.0.1: + resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.0.0 + '@tiptap/extension-bubble-menu': ^3.0.0 + '@tiptap/extension-floating-menu': ^3.0.0 + '@tiptap/pm': ^3.0.0 + svelte: ^5.0.0 + + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte-toolbelt@0.7.1: + resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + + svelte@5.55.4: + resolution: {integrity: sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==} + engines: {node: '>=18'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1612,10 +3054,20 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -1646,15 +3098,31 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} @@ -1665,6 +3133,10 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -1675,6 +3147,23 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-polyfill@1.1.14: + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + valibot@1.3.1: + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1718,6 +3207,14 @@ packages: yaml: optional: true + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@4.1.0: resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1753,6 +3250,26 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + web-haptics@0.0.6: + resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + svelte: '>=4' + vue: '>=3' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + vue: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1763,11 +3280,24 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + workerd@1.20260205.0: resolution: {integrity: sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==} engines: {node: '>=16'} hasBin: true + workerd@1.20260421.1: + resolution: {integrity: sha512-zTYD+xFR4d7TUCxsyl7FTPth9a8CDgk8pM7xUWbJxo0SGUx+2e5C7Q5LrramBZwmuAErtzXmOjlQ15PtkPAhZA==} + engines: {node: '>=16'} + hasBin: true + + worktop@0.8.0-next.18: + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} + engines: {node: '>=12'} + wrangler@4.63.0: resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} engines: {node: '>=20.0.0'} @@ -1778,6 +3308,16 @@ packages: '@cloudflare/workers-types': optional: true + wrangler@4.84.1: + resolution: {integrity: sha512-Xe1S/Bik7pNdtdJ+asHsEZC2dX9k3WxYn2BbxFtOrrLVxN/LKi750zsrjX41jSAk00M/O1l7jzyQV4sQqw8ftg==} + engines: {node: '>=20.3.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260421.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -1794,6 +3334,14 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -1804,12 +3352,26 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + snapshots: '@atcute/atproto@3.1.10': dependencies: '@atcute/lexicons': 1.2.9 + '@atcute/atproto@3.1.11': + dependencies: + '@atcute/lexicons': 1.3.0 + + '@atcute/bluesky-richtext-segmenter@3.0.0': {} + + '@atcute/bluesky@3.3.3': + dependencies: + '@atcute/atproto': 3.1.11 + '@atcute/lexicons': 1.3.0 + '@atcute/car@5.1.1': dependencies: '@atcute/cbor': 2.3.2 @@ -1888,7 +3450,7 @@ snapshots: '@atcute/identity': 1.1.4 '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) '@atcute/lexicon-doc': 2.1.2 - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@atcute/repo': 0.1.3 '@atcute/util-fetch': 1.0.5 '@badrap/valita': 0.4.6 @@ -1917,16 +3479,53 @@ snapshots: dependencies: '@atcute/uint8array': 1.1.1 + '@atcute/oauth-crypto@0.1.0': + dependencies: + '@atcute/multibase': 1.2.0 + '@atcute/uint8array': 1.1.1 + '@badrap/valita': 0.4.6 + nanoid: 5.1.7 + + '@atcute/oauth-keyset@0.1.0': + dependencies: + '@atcute/oauth-crypto': 0.1.0 + + '@atcute/oauth-node-client@1.1.0': + dependencies: + '@atcute/client': 4.2.1 + '@atcute/identity': 1.1.4 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/lexicons': 1.3.0 + '@atcute/oauth-crypto': 0.1.0 + '@atcute/oauth-keyset': 0.1.0 + '@atcute/oauth-types': 0.1.1 + '@atcute/util-fetch': 1.0.5 + '@badrap/valita': 0.4.6 + nanoid: 5.1.7 + + '@atcute/oauth-types@0.1.1': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/lexicons': 1.3.0 + '@atcute/oauth-keyset': 0.1.0 + '@badrap/valita': 0.4.6 + '@atcute/repo@0.1.3': dependencies: '@atcute/car': 5.1.1 '@atcute/cbor': 2.3.2 '@atcute/cid': 2.4.1 '@atcute/crypto': 2.4.1 - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@atcute/mst': 1.0.0 '@atcute/uint8array': 1.1.1 + '@atcute/tid@1.1.2': + dependencies: + '@atcute/time-ms': 1.3.2 + + '@atcute/time-ms@1.3.2': {} + '@atcute/uint8array@1.1.1': {} '@atcute/util-fetch@1.0.5': @@ -2110,23 +3709,46 @@ snapshots: optionalDependencies: workerd: 1.20260205.0 + '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260421.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260421.1 + '@cloudflare/workerd-darwin-64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-64@1.20260421.1': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20260421.1': + optional: true + '@cloudflare/workerd-linux-64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-64@1.20260421.1': + optional: true + '@cloudflare/workerd-linux-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20260421.1': + optional: true + '@cloudflare/workerd-windows-64@1.20260205.0': optional: true + '@cloudflare/workerd-windows-64@1.20260421.1': + optional: true + '@cloudflare/workers-types@4.20260205.0': {} + '@cloudflare/workers-types@4.20260422.1': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -2155,80 +3777,331 @@ snapshots: '@esbuild/aix-ppc64@0.27.0': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.27.0': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.27.0': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.27.0': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.27.0': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.27.0': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.27.0': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.27.0': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.27.0': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.27.0': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.27.0': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.27.0': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.27.0': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.27.0': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.27.0': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.27.0': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.27.0': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.27.0': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.27.0': optional: true - '@esbuild/openbsd-arm64@0.27.0': - optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.0': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.0': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.0': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.0': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.0': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))': + dependencies: + eslint: 10.2.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@2.0.5(eslint@10.2.1(jiti@2.6.1))': + dependencies: + '@eslint/core': 1.2.1 + optionalDependencies: + eslint: 10.2.1(jiti@2.6.1) + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.2.1(jiti@2.6.1))': + optionalDependencies: + eslint: 10.2.1(jiti@2.6.1) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@fontsource-variable/geist-mono@5.2.7': {} + + '@fontsource-variable/geist@5.2.8': {} - '@esbuild/openbsd-x64@0.27.0': - optional: true + '@foxui/core@0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + dependencies: + '@fontsource-variable/geist': 5.2.8 + '@fontsource-variable/geist-mono': 5.2.7 + '@jis3r/icons': 2.7.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + '@lucide/svelte': 1.8.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + clsx: 2.1.1 + dompurify: 3.4.1 + mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + tailwind-merge: 3.5.0 + tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.4) + tailwindcss: 4.2.4 + web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue + + '@foxui/social@0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + dependencies: + '@atcute/bluesky': 3.3.3 + '@atcute/bluesky-richtext-segmenter': 3.0.0 + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/text': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/time': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@use-gesture/vanilla': 10.3.1 + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + emoji-picker-element: 1.29.1 + hls.js: 1.6.16 + is-emoji-supported: 0.0.5 + plyr: 3.8.4 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + tailwindcss: 4.2.4 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - '@tiptap/extension-code-block' + - '@tiptap/extensions' + - highlight.js + - react + - react-dom + - vue + + '@foxui/text@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-code-block-lowlight': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@tiptap/extension-document': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-dropcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-floating-menu': 3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-history': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-image': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-link': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-paragraph': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-placeholder': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-strike': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-text': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-typography': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-underline': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/markdown': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + '@tiptap/starter-kit': 3.22.4 + '@tiptap/suggestion': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + lowlight: 3.3.0 + state: link:@tiptap/pm/state + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + tailwindcss: 4.2.4 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - '@tiptap/extension-code-block' + - '@tiptap/extensions' + - highlight.js + - react + - react-dom + - vue + + '@foxui/time@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + dependencies: + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + tailwindcss: 4.2.4 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue - '@esbuild/openharmony-arm64@0.27.0': - optional: true + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - '@esbuild/sunos-x64@0.27.0': - optional: true + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 - '@esbuild/win32-arm64@0.27.0': - optional: true + '@humanfs/types@0.15.0': {} - '@esbuild/win32-ia32@0.27.0': - optional: true + '@humanwhocodes/module-importer@1.0.1': {} - '@esbuild/win32-x64@0.27.0': - optional: true + '@humanwhocodes/retry@0.4.3': {} '@img/colour@1.0.0': {} @@ -2333,11 +4206,24 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 + '@internationalized/date@3.12.1': + dependencies: + '@swc/helpers': 0.5.21 + + '@jis3r/icons@2.7.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': + dependencies: + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2352,6 +4238,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lucide/svelte@1.8.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': + dependencies: + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -2395,6 +4285,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@number-flow/svelte@0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': + dependencies: + esm-env: 1.2.2 + number-flow: 0.6.0 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + '@optique/core@0.6.11': {} '@optique/run@0.6.11': @@ -2403,6 +4299,8 @@ snapshots: '@oxc-project/types@0.124.0': {} + '@polka/url@1.0.0-next.29': {} + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -2547,6 +4445,310 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1))': + dependencies: + '@cloudflare/workers-types': 4.20260422.1 + '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + worktop: 0.8.0-next.18 + wrangler: 4.84.1(@cloudflare/workers-types@4.20260422.1) + + '@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@types/cookie': 0.6.0 + acorn: 8.16.0 + cookie: 0.6.0 + devalue: 5.7.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.1.0 + sirv: 3.0.2 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + optionalDependencies: + typescript: 6.0.3 + + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + vitefu: 1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.4)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.2.4 + + '@tailwindcss/node@4.2.4': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.4 + + '@tailwindcss/oxide-android-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide@4.2.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-x64': 4.2.4 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + + '@tailwindcss/vite@4.2.4(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + tailwindcss: 4.2.4 + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + + '@tiptap/core@3.22.4(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-blockquote@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-bold@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-bullet-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-code-block-lowlight@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(highlight.js@11.11.1)(lowlight@3.3.0)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/extension-code-block': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + highlight.js: 11.11.1 + lowlight: 3.3.0 + + '@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-code@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-document@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-dropcursor@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-gapcursor@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-hard-break@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-heading@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-history@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-horizontal-rule@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-image@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-italic@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-link@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-list-keymap@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/extension-ordered-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-paragraph@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-placeholder@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + + '@tiptap/extension-strike@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-text@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-typography@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extension-underline@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + + '@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/markdown@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + marked: 17.0.6 + + '@tiptap/pm@3.22.4': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/starter-kit@3.22.4': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/extension-blockquote': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-bold': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-bullet-list': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-code': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-code-block': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-document': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-dropcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-gapcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-hard-break': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-heading': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-horizontal-rule': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-italic': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-link': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-list-item': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-list-keymap': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-ordered-list': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) + '@tiptap/extension-paragraph': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-strike': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-text': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extension-underline': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) + '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + + '@tiptap/suggestion@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + dependencies: + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2557,10 +4759,20 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json-schema@7.0.15': {} + '@types/node@12.20.55': {} '@types/node@25.5.0': @@ -2573,6 +4785,107 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/trusted-types@2.0.7': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 10.2.1(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + eslint: 10.2.1(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.2.1(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.0': {} + + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + eslint: 10.2.1(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/vanilla@10.3.1': + dependencies: + '@use-gesture/core': 10.3.1 + '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 @@ -2614,8 +4927,19 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2628,16 +4952,39 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.1: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} + axobject-query@4.1.0: {} + + balanced-match@4.0.4: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + bits-ui@2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/dom': 1.7.6 + '@internationalized/date': 3.12.1 + esm-env: 1.2.2 + runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@sveltejs/kit' + blake3-wasm@2.1.5: {} + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2657,6 +5004,8 @@ snapshots: dependencies: readdirp: 4.1.2 + clsx@2.1.1: {} + commander@4.1.1: {} confbox@0.1.8: {} @@ -2665,26 +5014,57 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.6.0: {} + cookie@1.1.1: {} + core-js@3.49.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + + custom-event-polyfill@1.0.7: {} + debug@4.4.3: dependencies: ms: 2.1.3 + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + dequal@2.0.3: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} + devalue@5.7.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 + dompurify@3.4.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + emoji-picker-element@1.29.1: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -2723,20 +5103,160 @@ snapshots: '@esbuild/win32-ia32': 0.27.0 '@esbuild/win32-x64': 0.27.0 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.6.1)): + dependencies: + eslint: 10.2.1(jiti@2.6.1) + + eslint-plugin-svelte@3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 10.2.1(jiti@2.6.1) + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.8 + postcss-load-config: 3.1.4(postcss@8.5.8) + postcss-safe-parser: 7.0.1(postcss@8.5.8) + semver: 7.7.4 + svelte-eslint-parser: 1.6.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + optionalDependencies: + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.2.1(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + esm-env@1.2.2: {} + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.5(@typescript-eslint/types@8.59.0): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.59.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + event-target-polyfill@0.0.4: {} expect-type@1.3.0: {} extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2745,6 +5265,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -2753,6 +5277,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2762,12 +5290,24 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.2 rollup: 4.60.1 + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2791,6 +5331,14 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@16.5.0: {} + + globals@17.5.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -2802,6 +5350,10 @@ snapshots: graceful-fs@4.2.11: {} + highlight.js@11.11.1: {} + + hls.js@1.6.16: {} + hono@4.12.8: {} human-id@4.1.3: {} @@ -2812,6 +5364,14 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + inline-style-parser@0.2.7: {} + + is-emoji-supported@0.0.5: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2820,6 +5380,10 @@ snapshots: is-number@7.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -2828,8 +5392,7 @@ snapshots: isexe@2.0.0: {} - jiti@2.6.1: - optional: true + jiti@2.6.1: {} joycon@3.1.1: {} @@ -2842,12 +5405,29 @@ snapshots: dependencies: argparse: 2.0.1 + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@4.1.5: {} + known-css-properties@0.37.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -2897,22 +5477,44 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@2.1.0: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + linkifyjs@4.3.2: {} + load-tsconfig@0.2.5: {} + loadjs@4.3.0: {} + + locate-character@3.0.0: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.startcase@4.4.0: {} + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@17.0.6: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2920,6 +5522,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mini-svg-data-uri@1.4.4: {} + miniflare@4.20260205.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -2932,6 +5536,22 @@ snapshots: - bufferutil - utf-8-validate + miniflare@4.20260421.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260421.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -2939,8 +5559,16 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + mode-watcher@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + runed: 0.25.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte-toolbelt: 0.7.1(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} mz@2.7.0: @@ -2953,10 +5581,27 @@ snapshots: nanoid@5.1.7: {} + natural-compare@1.4.0: {} + + number-flow@0.6.0: + dependencies: + esm-env: 1.2.2 + object-assign@4.1.1: {} obug@2.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + orderedmap@2.1.1: {} + outdent@0.5.0: {} p-filter@2.1.0: @@ -2967,10 +5612,18 @@ snapshots: dependencies: p-try: 2.2.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-map@2.1.0: {} p-try@2.2.0: {} @@ -3046,6 +5699,21 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 + plyr@3.8.4: + dependencies: + core-js: 3.49.0 + custom-event-polyfill: 1.0.7 + loadjs: 4.3.0 + rangetouch: 2.0.1 + url-polyfill: 1.1.14 + + postcss-load-config@3.1.4(postcss@8.5.8): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.3 + optionalDependencies: + postcss: 8.5.8 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 @@ -3054,6 +5722,19 @@ snapshots: postcss: 8.5.8 tsx: 4.21.0 + postcss-safe-parser@7.0.1(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-scss@4.0.9(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -3070,14 +5751,100 @@ snapshots: dependencies: xtend: 4.0.2 + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + prettier: 3.8.1 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)))(prettier@3.8.1): + dependencies: + prettier: 3.8.1 + optionalDependencies: + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + prettier@2.8.8: {} prettier@3.8.1: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + punycode@2.3.1: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {} + rangetouch@2.0.1: {} + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -3087,6 +5854,8 @@ snapshots: readdirp@4.1.2: {} + regexparam@3.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3145,14 +5914,46 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + runed@0.23.4(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + runed@0.25.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + runed@0.28.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + runed@0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + optionalDependencies: + '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + + sade@1.8.1: + dependencies: + mri: 1.2.0 + safer-buffer@2.1.2: {} semver@7.7.4: {} + set-cookie-parser@3.1.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -3194,6 +5995,12 @@ snapshots: signal-exit@4.1.0: {} + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} source-map-js@1.2.1: {} @@ -3219,6 +6026,10 @@ snapshots: strip-bom@3.0.0: {} + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -3231,6 +6042,95 @@ snapshots: supports-color@10.2.2: {} + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.6.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.8 + postcss-scss: 4.0.9(postcss@8.5.8) + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + optionalDependencies: + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + svelte-sonner@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + runed: 0.28.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/extension-floating-menu': 3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/pm': 3.22.4 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + style-to-object: 1.0.14 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + transitivePeerDependencies: + - '@sveltejs/kit' + + svelte-toolbelt@0.7.1(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + dependencies: + clsx: 2.1.1 + runed: 0.23.4(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + style-to-object: 1.0.14 + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + + svelte@5.55.4(@typescript-eslint/types@8.59.0): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.7.1 + esm-env: 1.2.2 + esrap: 2.2.5(@typescript-eslint/types@8.59.0) + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tabbable@6.4.0: {} + + tailwind-merge@3.5.0: {} + + tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.4): + dependencies: + tailwindcss: 4.2.4 + optionalDependencies: + tailwind-merge: 3.5.0 + + tailwindcss@4.2.4: {} + + tapable@2.3.3: {} + term-size@2.2.1: {} thenify-all@1.6.0: @@ -3258,12 +6158,17 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tree-kill@1.2.2: {} + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + ts-interface-checker@0.1.13: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): dependencies: @@ -3300,16 +6205,35 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@4.41.0: {} + typescript-eslint@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + eslint: 10.2.1(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} + typescript@6.0.3: {} + ufo@1.6.3: {} undici-types@7.18.2: {} undici@7.18.2: {} + undici@7.24.8: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -3318,6 +6242,18 @@ snapshots: universalify@0.1.2: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-polyfill@1.1.14: {} + + util-deprecate@1.0.2: {} + + valibot@1.3.1(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 @@ -3332,6 +6268,24 @@ snapshots: jiti: 2.6.1 tsx: 4.21.0 + vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + esbuild: 0.27.3 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + + vitefu@1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): + optionalDependencies: + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + vitest@4.1.0(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.0 @@ -3359,6 +6313,12 @@ snapshots: transitivePeerDependencies: - msw + w3c-keyname@2.2.8: {} + + web-haptics@0.0.6(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + optionalDependencies: + svelte: 5.55.4(@typescript-eslint/types@8.59.0) + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3368,6 +6328,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + workerd@1.20260205.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260205.0 @@ -3376,6 +6338,19 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260205.0 '@cloudflare/workerd-windows-64': 1.20260205.0 + workerd@1.20260421.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260421.1 + '@cloudflare/workerd-darwin-arm64': 1.20260421.1 + '@cloudflare/workerd-linux-64': 1.20260421.1 + '@cloudflare/workerd-linux-arm64': 1.20260421.1 + '@cloudflare/workerd-windows-64': 1.20260421.1 + + worktop@0.8.0-next.18: + dependencies: + mrmime: 2.0.1 + regexparam: 3.0.0 + wrangler@4.63.0(@cloudflare/workers-types@4.20260205.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -3393,10 +6368,31 @@ snapshots: - bufferutil - utf-8-validate + wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260421.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260421.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260421.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260422.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + ws@8.18.0: {} xtend@4.0.2: {} + yaml@1.10.3: {} + + yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} youch-core@0.3.3: @@ -3411,3 +6407,5 @@ snapshots: '@speed-highlight/core': 1.2.14 cookie: 1.1.1 youch-core: 0.3.3 + + zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ea1b5b..b92d403 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,6 @@ +packages: + - "examples/sveltekit-group-chat" + ignoredBuiltDependencies: - esbuild - sharp diff --git a/src/core/community/adapter.ts b/src/core/community/adapter.ts index ae9e62a..1587e63 100644 --- a/src/core/community/adapter.ts +++ b/src/core/community/adapter.ts @@ -1,5 +1,12 @@ import type { Database } from "../types"; -import type { AccessLevel, AccessLevelRow, CommunityMode, CommunityRow } from "./types"; +import type { + AccessLevel, + AccessLevelRow, + CommunityInviteRow, + CommunityMode, + CommunityRow, + CreateCommunityInviteInput, +} from "./types"; function toNum(v: unknown): number { return typeof v === "string" ? Number(v) : (v as number); @@ -286,4 +293,110 @@ export class CommunityAdapter { .bind(spaceUri) .run(); } + + // ---- Invites ----------------------------------------------------------- + + async createInvite(input: CreateCommunityInviteInput): Promise { + const now = Date.now(); + await this.db + .prepare( + `INSERT INTO community_invites + (token_hash, space_uri, access_level, created_by, created_at, expires_at, max_uses, used_count, note) + VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)` + ) + .bind( + input.tokenHash, + input.spaceUri, + input.accessLevel, + input.createdBy, + now, + input.expiresAt, + input.maxUses, + input.note + ) + .run(); + return { + tokenHash: input.tokenHash, + spaceUri: input.spaceUri, + accessLevel: input.accessLevel, + createdBy: input.createdBy, + createdAt: now, + expiresAt: input.expiresAt, + maxUses: input.maxUses, + usedCount: 0, + revokedAt: null, + note: input.note, + }; + } + + async getInvite(tokenHash: string): Promise { + const row = await this.db + .prepare(`SELECT * FROM community_invites WHERE token_hash = ?`) + .bind(tokenHash) + .first(); + return row ? mapCommunityInviteRow(row) : null; + } + + async listInvites( + spaceUri: string, + options: { includeRevoked?: boolean } = {} + ): Promise { + const sql = options.includeRevoked + ? `SELECT * FROM community_invites WHERE space_uri = ? ORDER BY created_at DESC` + : `SELECT * FROM community_invites WHERE space_uri = ? AND revoked_at IS NULL ORDER BY created_at DESC`; + const { results } = await this.db.prepare(sql).bind(spaceUri).all(); + return results.map(mapCommunityInviteRow); + } + + async revokeInvite(tokenHash: string): Promise { + const res = await this.db + .prepare( + `UPDATE community_invites SET revoked_at = ? WHERE token_hash = ? AND revoked_at IS NULL` + ) + .bind(Date.now(), tokenHash) + .run(); + const changes = (res as any)?.changes ?? (res as any)?.meta?.changes ?? 0; + return Number(changes) > 0; + } + + /** Atomically consume one "use" from an invite. Returns the updated row if + * the invite is currently usable, null otherwise (expired, revoked, + * exhausted, or missing). The router calls this, then issues a grant if + * the return is non-null. */ + async redeemInvite(tokenHash: string, now: number): Promise { + const res = await this.db + .prepare( + `UPDATE community_invites + SET used_count = used_count + 1 + WHERE token_hash = ? + AND revoked_at IS NULL + AND (expires_at IS NULL OR expires_at > ?) + AND (max_uses IS NULL OR used_count < max_uses)` + ) + .bind(tokenHash, now) + .run(); + const changes = (res as any)?.changes ?? (res as any)?.meta?.changes ?? 0; + if (Number(changes) === 0) return null; + + const row = await this.db + .prepare(`SELECT * FROM community_invites WHERE token_hash = ?`) + .bind(tokenHash) + .first(); + return row ? mapCommunityInviteRow(row) : null; + } +} + +function mapCommunityInviteRow(row: any): CommunityInviteRow { + return { + tokenHash: row.token_hash, + spaceUri: row.space_uri, + accessLevel: row.access_level as AccessLevel, + createdBy: row.created_by, + createdAt: toNum(row.created_at), + expiresAt: row.expires_at == null ? null : toNum(row.expires_at), + maxUses: row.max_uses == null ? null : Number(row.max_uses), + usedCount: Number(row.used_count ?? 0), + revokedAt: row.revoked_at == null ? null : toNum(row.revoked_at), + note: row.note ?? null, + }; } diff --git a/src/core/community/index.ts b/src/core/community/index.ts index 5d3852e..3fbe1ad 100644 --- a/src/core/community/index.ts +++ b/src/core/community/index.ts @@ -5,8 +5,10 @@ export type { AccessLevel, AccessLevelRow, CommunityConfig, + CommunityInviteRow, CommunityMode, CommunityRow, + CreateCommunityInviteInput, ReservedKey, } from "./types"; export { diff --git a/src/core/community/plc.ts b/src/core/community/plc.ts index b5dcc1c..b4d1c5b 100644 --- a/src/core/community/plc.ts +++ b/src/core/community/plc.ts @@ -48,6 +48,12 @@ export function jwkToDidKey(jwk: JsonWebKey): string { return "did:key:z" + base58btcEncode(combined); } +/** Order of the P-256 curve (n). */ +const P256_N = BigInt( + "0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551" +); +const P256_N_HALF = P256_N >> 1n; + async function signBytes(privateJwk: JsonWebKey, bytes: Uint8Array): Promise { const key = await crypto.subtle.importKey( "jwk", @@ -56,12 +62,44 @@ async function signBytes(privateJwk: JsonWebKey, bytes: Uint8Array): Promise= 0; i--) { + out[i] = Number(x & 0xffn); + x >>= 8n; + } + return out; } // ============================================================================ diff --git a/src/core/community/router.ts b/src/core/community/router.ts index c46acc5..0318d7e 100644 --- a/src/core/community/router.ts +++ b/src/core/community/router.ts @@ -25,6 +25,10 @@ import { RESERVED_KEYS, } from "./types"; import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; +import { + generateInviteToken, + hashInviteToken, +} from "../spaces/invite-token"; export interface CommunityRoutesOptions { /** Override auth middleware for tests. */ @@ -971,6 +975,166 @@ export function registerCommunityRoutes( }); return c.json({ ok: true }); }); + + // ========================================================================== + // Invites — pre-signed grants for community-owned spaces. Admin/manager + // creates; any authenticated user with the raw token redeems once. + // ========================================================================== + + app.post(`/xrpc/${NS}.invite.create`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + spaceUri?: string; + accessLevel?: string; + expiresAt?: number; + maxUses?: number; + note?: string; + } + | null; + if (!body?.spaceUri || !body.accessLevel) { + return c.json( + { error: "InvalidRequest", message: "spaceUri and accessLevel required" }, + 400 + ); + } + if (!isAccessLevel(body.accessLevel)) { + return c.json({ error: "InvalidRequest", message: "invalid accessLevel" }, 400); + } + const space = await spaces.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + const communityRow = await community.getCommunity(space.ownerDid); + if (!communityRow) { + return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); + } + + // Caller must have at least manager on the target space, and cannot create + // an invite that confers a higher level than their own. + const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + if (rankOf(body.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); + } + + const token = generateInviteToken(); + const tokenHash = await hashInviteToken(token); + + const row = await community.createInvite({ + spaceUri: body.spaceUri, + tokenHash, + accessLevel: body.accessLevel, + createdBy: sa.issuer, + expiresAt: body.expiresAt ?? null, + maxUses: body.maxUses ?? null, + note: body.note ?? null, + }); + + // Raw token only returned once. tokenHash is the stable ID for list/revoke. + return c.json({ + token, + tokenHash, + spaceUri: row.spaceUri, + accessLevel: row.accessLevel, + expiresAt: row.expiresAt, + maxUses: row.maxUses, + createdAt: row.createdAt, + }); + }); + + app.get(`/xrpc/${NS}.invite.list`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + const includeRevoked = c.req.query("includeRevoked") === "true"; + if (!spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + const space = await spaces.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const callerLevel = await resolveEffectiveLevel(community, spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + + const rows = await community.listInvites(spaceUri, { includeRevoked }); + return c.json({ + invites: rows.map((r) => ({ + tokenHash: r.tokenHash, + spaceUri: r.spaceUri, + accessLevel: r.accessLevel, + createdBy: r.createdBy, + createdAt: r.createdAt, + expiresAt: r.expiresAt, + maxUses: r.maxUses, + usedCount: r.usedCount, + revokedAt: r.revokedAt, + note: r.note, + })), + }); + }); + + app.post(`/xrpc/${NS}.invite.revoke`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { tokenHash?: string } + | null; + if (!body?.tokenHash) { + return c.json({ error: "InvalidRequest", message: "tokenHash required" }, 400); + } + const invite = await community.getInvite(body.tokenHash); + if (!invite) return c.json({ error: "NotFound" }, 404); + + // Revoker: either the invite's creator, or manager+ on the target space. + let allowed = invite.createdBy === sa.issuer; + if (!allowed) { + const level = await resolveEffectiveLevel(community, invite.spaceUri, sa.issuer); + allowed = !!level && rankOf(level) >= rankOf("manager"); + } + if (!allowed) { + return c.json({ error: "Forbidden", reason: "creator-or-manager-required" }, 403); + } + await community.revokeInvite(body.tokenHash); + return c.json({ ok: true }); + }); + + app.post(`/xrpc/${NS}.invite.redeem`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as { token?: string } | null; + if (!body?.token) { + return c.json({ error: "InvalidRequest", message: "token required" }, 400); + } + const tokenHash = await hashInviteToken(body.token); + + // Atomic consume — returns null if the token is expired/revoked/exhausted. + const invite = await community.redeemInvite(tokenHash, Date.now()); + if (!invite) { + return c.json({ error: "InvalidToken", reason: "token-invalid-or-exhausted" }, 410); + } + + const space = await spaces.getSpace(invite.spaceUri); + if (!space) { + return c.json({ error: "NotFound", reason: "space-not-found" }, 404); + } + + // The token itself is the authorization: the creator (who had manager+) + // pre-signed "anyone with this token gets level X on this space". Grant + // directly, attributing to the creator so audit trails make sense. + await community.grant({ + spaceUri: invite.spaceUri, + subjectDid: sa.issuer, + accessLevel: invite.accessLevel, + grantedBy: invite.createdBy, + }); + await reconcile(community, spaces, invite.spaceUri, invite.createdBy); + + return c.json({ + spaceUri: invite.spaceUri, + accessLevel: invite.accessLevel, + communityDid: space.ownerDid, + }); + }); } // ============================================================================ diff --git a/src/core/community/schema.ts b/src/core/community/schema.ts index ef5e16d..b7d382c 100644 --- a/src/core/community/schema.ts +++ b/src/core/community/schema.ts @@ -30,6 +30,20 @@ export function buildCommunitySchema(dialect: SqlDialect): string[] { `CREATE INDEX IF NOT EXISTS idx_cal_subject ON community_access_levels(subject)`, `CREATE INDEX IF NOT EXISTS idx_cal_subject_space ON community_access_levels(subject) WHERE subject_kind = 'space'`, + + `CREATE TABLE IF NOT EXISTS community_invites ( + token_hash TEXT PRIMARY KEY NOT NULL, + space_uri TEXT NOT NULL, + access_level TEXT NOT NULL, + created_by TEXT NOT NULL, + created_at ${dialect.bigintType} NOT NULL, + expires_at ${dialect.bigintType}, + max_uses INTEGER, + used_count INTEGER NOT NULL DEFAULT 0, + revoked_at ${dialect.bigintType}, + note TEXT + )`, + `CREATE INDEX IF NOT EXISTS idx_community_invites_space ON community_invites(space_uri, created_at DESC)`, ]; } diff --git a/src/core/community/types.ts b/src/core/community/types.ts index 9cf690f..7959e72 100644 --- a/src/core/community/types.ts +++ b/src/core/community/types.ts @@ -59,6 +59,32 @@ export interface AccessLevelRow { grantedAt: number; } +/** A pre-signed grant for a community-owned space. An admin/manager creates + * one; anyone with the raw token can redeem it once to get the specified + * access level. Storage keeps only the SHA-256 of the token. */ +export interface CommunityInviteRow { + tokenHash: string; + spaceUri: string; + accessLevel: AccessLevel; + createdBy: string; + createdAt: number; + expiresAt: number | null; + maxUses: number | null; + usedCount: number; + revokedAt: number | null; + note: string | null; +} + +export interface CreateCommunityInviteInput { + spaceUri: string; + tokenHash: string; + accessLevel: AccessLevel; + createdBy: string; + expiresAt: number | null; + maxUses: number | null; + note: string | null; +} + /** Key prefix for reserved community-owned spaces. */ export const RESERVED_KEYS = ["$admin", "$publishers"] as const; export type ReservedKey = (typeof RESERVED_KEYS)[number]; diff --git a/src/core/realtime/durable-object.ts b/src/core/realtime/durable-object.ts index 6dc6964..966ab53 100644 --- a/src/core/realtime/durable-object.ts +++ b/src/core/realtime/durable-object.ts @@ -15,6 +15,21 @@ * the DO trusts anything that reaches it. */ import type { PubSub, RealtimeEvent } from "./types"; +import { translateForQuery, type TranslatedEnvelope } from "./query-filter"; +type TranslatedEvent = TranslatedEnvelope; + +/** Query spec attached to a WS subscriber, used to filter events before + * delivery. Shape matches what the Worker's `watchRecords` handler builds; + * forwarded to the DO via trusted internal headers on the WS upgrade. */ +export interface SubscriberQuerySpec { + /** NSID of the primary collection the client is watching. */ + collection: string; + /** Space URI this subscription is scoped to. Events outside are dropped. */ + spaceUri: string; + /** Hydrated relations. Keyed by relName — value is the child collection + * NSID and the field on the child record that references the parent. */ + hydrate?: Record; +} // ---- Minimal structural typings so we don't depend on @cloudflare/workers-types // at the library level. Callers on Workers will have proper types. @@ -73,6 +88,36 @@ export class DurableObjectPubSub implements PubSub { }, }; } + + /** Forward an incoming browser WS upgrade (or SSE GET) through to the DO + * that owns this topic, attaching a query-filter spec that the DO will use + * to decide what to deliver. The Worker must verify auth + spec validity + * before calling this — the DO trusts the headers. */ + async forwardSubscribe( + topic: string, + request: Request, + opts: { + did?: string; + querySpec?: SubscriberQuerySpec; + /** Unix ms. DO replays any buffered event with ts > sinceTs before + * going live — closes the snapshot→WS race window on the client. */ + sinceTs?: number; + } = {} + ): Promise { + const headers = new Headers(request.headers); + if (opts.querySpec) { + headers.set("X-Contrail-Query-Spec", JSON.stringify(opts.querySpec)); + } + const url = new URL("https://do/subscribe"); + if (opts.did) url.searchParams.set("did", opts.did); + if (opts.sinceTs && opts.sinceTs > 0) { + url.searchParams.set("sinceTs", String(opts.sinceTs)); + } + return this.stub(topic).fetch(url.toString(), { + method: "GET", + headers + }); + } } function pullIterator( @@ -150,12 +195,32 @@ function pullIterator( * This class intentionally avoids the `DurableObject` base class so we don't * have to depend on @cloudflare/workers-types at the library level — users * wire it up directly in their Worker entry. */ +/** Rolling buffer of recent events, used to close the snapshot→WS race: + * when a new subscriber connects with `?sinceTs=X`, replay any buffered + * event with `event.ts > X` before going live. Bounded by count + age so + * memory stays small. */ +const RECENT_BUFFER_MS = 15_000; +const RECENT_BUFFER_MAX = 500; + export class RealtimePubSubDO { + private readonly recentEvents: RealtimeEvent[] = []; + constructor( protected readonly state: DurableObjectState, _env?: unknown ) {} + private pushRecent(event: RealtimeEvent): void { + this.recentEvents.push(event); + const cutoff = Date.now() - RECENT_BUFFER_MS; + while ( + this.recentEvents.length > RECENT_BUFFER_MAX || + (this.recentEvents.length > 0 && this.recentEvents[0]!.ts < cutoff) + ) { + this.recentEvents.shift(); + } + } + /** Worker entry delegates `fetch` to this method. */ async fetch(request: Request): Promise { const url = new URL(request.url); @@ -171,35 +236,79 @@ export class RealtimePubSubDO { } if (request.method === "GET" && url.pathname === "/subscribe") { const did = url.searchParams.get("did") ?? undefined; + const sinceTsRaw = url.searchParams.get("sinceTs"); + const sinceTs = sinceTsRaw ? Number(sinceTsRaw) : 0; + // Optional query-filter spec, forwarded by the Worker after it has + // verified the caller's auth + access. Parsed once here; the parsed + // object is serialized into the WS attachment so the DO can filter + // events on publish without re-parsing. + let querySpec: SubscriberQuerySpec | undefined; + const rawSpec = request.headers.get("X-Contrail-Query-Spec"); + if (rawSpec) { + try { + querySpec = JSON.parse(rawSpec) as SubscriberQuerySpec; + } catch { + return new Response( + JSON.stringify({ error: "InvalidRequest", message: "bad X-Contrail-Query-Spec" }), + { status: 400 } + ); + } + } + if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") { const Pair = (globalThis as unknown as { WebSocketPair?: any }).WebSocketPair; if (!Pair) return new Response("websockets require Workers", { status: 426 }); const pair = new Pair(); - this.acceptWebSocketSubscriber(pair[1], did); + this.acceptWebSocketSubscriber(pair[1], did, querySpec); + if (sinceTs > 0) this.replayRecentTo(pair[1], sinceTs); return new Response(null, { status: 101, - // @ts-expect-error Workers-specific init field + // Workers-specific init field webSocket: pair[0], - }); + } as ResponseInit & { webSocket: unknown }); } - return this.openSseResponse(did); + return this.openSseResponse(did, querySpec, sinceTs); } return new Response("not found", { status: 404 }); } /** Fan-out an event to every connected subscriber (WS + SSE). - * Public so tests + advanced callers can skip the HTTP layer. */ + * Public so tests + advanced callers can skip the HTTP layer. + * + * If a subscriber has attached a `querySpec`, we translate the raw event + * into 0–1 watchRecords-shaped events (record.created, record.deleted, + * hydration.added, hydration.removed) and deliver only those. Otherwise + * the raw event is delivered as-is (topic-firehose behaviour for the + * `realtime.subscribe` endpoint). */ publishEvent(event: RealtimeEvent): void { - const payload = JSON.stringify(event); - const frame = `event: ${event.kind}\ndata: ${payload}\n\n`; + // Buffer first so a subscriber connecting mid-publish (race-window + // replay) can pick up this event too once they provide their sinceTs. + this.pushRecent(event); + + const rawPayload = JSON.stringify(event); + const rawFrame = `event: ${event.kind}\ndata: ${rawPayload}\n\n`; for (const ws of this.state.getWebSockets()) { - try { - ws.send(payload); - } catch { - /* ignore — socket may be closing */ - } const attachment = getAttachment(ws); + + if (attachment?.querySpec) { + const translated = translateForQuery(event, attachment); + if (translated) this.writeSubscriberState(ws, attachment, translated); + for (const msg of translated ?? []) { + try { + ws.send(JSON.stringify(msg)); + } catch { + /* ignore */ + } + } + } else { + try { + ws.send(rawPayload); + } catch { + /* ignore */ + } + } + if ( event.kind === "member.removed" && attachment?.did && @@ -212,12 +321,28 @@ export class RealtimePubSubDO { } } } + for (const entry of this.sseControllers) { - try { - entry.controller.enqueue(this.encoder.encode(frame)); - } catch { - /* drop; cleanup happens on the subscribe-side */ + if (entry.querySpec) { + const translated = translateForQuery(event, entry); + if (translated) this.writeSubscriberStateForSse(entry, translated); + for (const msg of translated ?? []) { + try { + entry.controller.enqueue( + this.encoder.encode(`event: ${msg.kind}\ndata: ${JSON.stringify(msg.data)}\n\n`) + ); + } catch { + /* drop */ + } + } + } else { + try { + entry.controller.enqueue(this.encoder.encode(rawFrame)); + } catch { + /* drop; cleanup happens on the subscribe-side */ + } } + if ( event.kind === "member.removed" && entry.did && @@ -232,20 +357,44 @@ export class RealtimePubSubDO { } } - /** Register a server-side WebSocket as a subscriber. Wires the DID attachment. */ - acceptWebSocketSubscriber(serverWs: any, did?: string): void { + /** Register a server-side WebSocket as a subscriber. Wires the DID + + * optional query spec into the hibernation attachment so this DO can + * filter and route events after going to sleep. */ + acceptWebSocketSubscriber( + serverWs: any, + did?: string, + querySpec?: SubscriberQuerySpec + ): void { this.state.acceptWebSocket(serverWs, did ? [did] : undefined); - if (did) setAttachment(serverWs, { did }); + if (did || querySpec) { + setAttachment(serverWs, { + did, + querySpec, + parentUris: [], + childToParent: {} + }); + } } /** Open an SSE subscriber; returns the streaming Response. */ - openSseResponse(did?: string): Response { + openSseResponse( + did?: string, + querySpec?: SubscriberQuerySpec, + sinceTs = 0 + ): Response { let entry: SseEntry; const stream = new ReadableStream({ start: (controller) => { - entry = { controller, did }; + entry = { + controller, + did, + querySpec, + parentUris: new Set(), + childToParent: new Map() + }; this.sseControllers.add(entry); controller.enqueue(this.encoder.encode(`: open\n\n`)); + if (sinceTs > 0) this.replayRecentToSse(entry, sinceTs); }, cancel: () => { this.sseControllers.delete(entry); @@ -261,6 +410,119 @@ export class RealtimePubSubDO { }); } + /** Replay buffered events with ts > sinceTs through this subscriber's + * query-spec filter. Called once, synchronously, on WS connect. */ + private replayRecentTo(ws: any, sinceTs: number): void { + const attachment = getAttachment(ws); + for (const event of this.recentEvents) { + if (event.ts <= sinceTs) continue; + if (attachment?.querySpec) { + const translated = translateForQuery(event, attachment); + if (translated) this.writeSubscriberState(ws, attachment, translated); + for (const msg of translated ?? []) { + try { + ws.send(JSON.stringify(msg)); + } catch { + /* ignore */ + } + } + } else { + try { + ws.send(JSON.stringify(event)); + } catch { + /* ignore */ + } + } + } + } + + private replayRecentToSse(entry: SseEntry, sinceTs: number): void { + for (const event of this.recentEvents) { + if (event.ts <= sinceTs) continue; + if (entry.querySpec) { + const translated = translateForQuery(event, entry); + if (translated) this.writeSubscriberStateForSse(entry, translated); + for (const msg of translated ?? []) { + try { + entry.controller.enqueue( + this.encoder.encode(`event: ${msg.kind}\ndata: ${JSON.stringify(msg.data)}\n\n`) + ); + } catch { + /* drop */ + } + } + } else { + try { + entry.controller.enqueue( + this.encoder.encode(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`) + ); + } catch { + /* drop */ + } + } + } + } + + /** Update the persisted WS attachment state after we've decided which + * events to forward. Keeps the parent/child tracking tables warm across + * hibernation. */ + private writeSubscriberState( + ws: any, + attachment: WsAttachment, + translated: TranslatedEvent[] + ): void { + let dirty = false; + for (const msg of translated) { + if (msg.kind === "record.created" && msg.data.record?.uri) { + attachment.parentUris = Array.from( + new Set([...(attachment.parentUris ?? []), msg.data.record.uri]) + ); + dirty = true; + } else if (msg.kind === "record.deleted" && msg.data.uri) { + const before = attachment.parentUris ?? []; + attachment.parentUris = before.filter((u) => u !== msg.data.uri); + if (attachment.parentUris.length !== before.length) dirty = true; + } else if (msg.kind === "hydration.added" && msg.data.child?.rkey) { + attachment.childToParent = { + ...(attachment.childToParent ?? {}), + [msg.data.child.rkey]: { + parentUri: msg.data.parentUri, + relName: msg.data.relation + } + }; + dirty = true; + } else if (msg.kind === "hydration.removed" && msg.data.childRkey) { + const next = { ...(attachment.childToParent ?? {}) }; + if (next[msg.data.childRkey]) { + delete next[msg.data.childRkey]; + attachment.childToParent = next; + dirty = true; + } + } + } + if (dirty) setAttachment(ws, attachment); + } + + private writeSubscriberStateForSse( + entry: SseEntry, + translated: TranslatedEvent[] + ): void { + for (const msg of translated) { + if (msg.kind === "record.created" && msg.data.record?.uri) { + entry.parentUris?.add(msg.data.record.uri); + } else if (msg.kind === "record.deleted" && msg.data.uri) { + entry.parentUris?.delete(msg.data.uri); + } else if (msg.kind === "hydration.added" && msg.data.child?.rkey) { + entry.childToParent?.set(msg.data.child.rkey, { + parentUri: msg.data.parentUri, + relName: msg.data.relation + }); + } else if (msg.kind === "hydration.removed" && msg.data.childRkey) { + entry.childToParent?.delete(msg.data.childRkey); + } + } + } + private readonly sseControllers = new Set(); private readonly encoder = new TextEncoder(); } @@ -268,10 +530,18 @@ export class RealtimePubSubDO { interface SseEntry { controller: ReadableStreamDefaultController; did: string | undefined; + querySpec?: SubscriberQuerySpec; + parentUris?: Set; + childToParent?: Map; } interface WsAttachment { did?: string; + querySpec?: SubscriberQuerySpec; + /** URIs of primary records currently in this subscriber's result set. */ + parentUris?: string[]; + /** childRkey → parent info, for routing child delete events. */ + childToParent?: Record; } function setAttachment(ws: any, attachment: WsAttachment): void { @@ -292,3 +562,6 @@ function getAttachment(ws: any): WsAttachment | null { } return (ws.__attachment as WsAttachment | undefined) ?? null; } + +// Query-spec filtering lives in ./query-filter so the Worker can reuse it for +// non-DO (InMemoryPubSub) watchRecords paths without bundling the whole DO. diff --git a/src/core/realtime/publishing-adapter.ts b/src/core/realtime/publishing-adapter.ts index 24c6e06..e96a226 100644 --- a/src/core/realtime/publishing-adapter.ts +++ b/src/core/realtime/publishing-adapter.ts @@ -55,6 +55,11 @@ export function wrapWithPublishing( getRecord: inner.getRecord.bind(inner), listRecords: inner.listRecords.bind(inner), listCollections: inner.listCollections.bind(inner), + putBlobMeta: inner.putBlobMeta.bind(inner), + getBlobMeta: inner.getBlobMeta.bind(inner), + listBlobMeta: inner.listBlobMeta.bind(inner), + deleteBlobMeta: inner.deleteBlobMeta.bind(inner), + findOrphanBlobs: inner.findOrphanBlobs.bind(inner), async addMember(spaceUri, did, addedBy) { await inner.addMember(spaceUri, did, addedBy); diff --git a/src/core/realtime/query-filter.ts b/src/core/realtime/query-filter.ts new file mode 100644 index 0000000..6806c45 --- /dev/null +++ b/src/core/realtime/query-filter.ts @@ -0,0 +1,235 @@ +/** Shared query-spec → event-translation logic. + * + * Used by: + * - the Durable Object's WS publish path (per-subscriber filter after hibernation) + * - the Worker's SSE / Worker-terminated WS path (in-process filter) + * + * Given a raw RealtimeEvent and a SubscriberQuerySpec, returns the list of + * `{kind, data}` envelopes to send to the subscriber, or `null` if the + * subscriber has no spec (i.e. raw-firehose mode). */ + +import type { RealtimeEvent } from "./types"; +import type { SubscriberQuerySpec } from "./durable-object"; + +export type TranslatedEnvelope = + | { + kind: "record.created"; + data: { + record: { + uri: string; + did: string; + rkey: string; + collection: string; + cid: string | null | undefined; + record: Record; + time_us: number; + indexed_at: number; + _space: string; + }; + }; + } + | { kind: "record.deleted"; data: { uri: string; did: string; rkey: string } } + | { + kind: "hydration.added"; + data: { + parentUri: string; + relation: string; + child: { + uri: string; + did: string; + rkey: string; + collection: string; + cid: string | null | undefined; + record: Record; + _space: string; + }; + }; + } + | { + kind: "hydration.removed"; + data: { + parentUri: string; + relation: string; + childRkey: string; + childDid?: string; + }; + }; + +export interface SubscriberView { + querySpec?: SubscriberQuerySpec; + parentUris?: Set | string[]; + childToParent?: + | Map + | Record; +} + +export function translateForQuery( + event: RealtimeEvent, + sub: SubscriberView +): TranslatedEnvelope[] | null { + const spec = sub.querySpec; + if (!spec) return null; + if (event.kind !== "record.created" && event.kind !== "record.deleted") return []; + if (event.payload.spaceUri !== spec.spaceUri) return []; + + const primaryUri = `at://${event.payload.authorDid}/${event.payload.collection}/${event.payload.rkey}`; + + if (event.payload.collection === spec.collection) { + if (event.kind === "record.created") { + return [ + { + kind: "record.created", + data: { + record: { + uri: primaryUri, + did: event.payload.authorDid, + rkey: event.payload.rkey, + collection: event.payload.collection, + cid: event.payload.cid, + record: event.payload.record, + time_us: event.ts * 1000, + indexed_at: event.ts, + _space: spec.spaceUri + } + } + } + ]; + } + return [ + { + kind: "record.deleted", + data: { + uri: primaryUri, + did: event.payload.authorDid, + rkey: event.payload.rkey + } + } + ]; + } + + if (!spec.hydrate) return []; + for (const [relName, rel] of Object.entries(spec.hydrate)) { + if (rel.childCollection !== event.payload.collection) continue; + if (event.kind === "record.created") { + const parentUri = getNestedValue( + event.payload.record as Record, + rel.matchField + ); + if (typeof parentUri !== "string") continue; + if (!hasParent(sub.parentUris, parentUri)) continue; + return [ + { + kind: "hydration.added", + data: { + parentUri, + relation: relName, + child: { + uri: primaryUri, + did: event.payload.authorDid, + rkey: event.payload.rkey, + collection: event.payload.collection, + cid: event.payload.cid, + record: event.payload.record, + _space: spec.spaceUri + } + } + } + ]; + } + const info = getChildInfo(sub.childToParent, event.payload.rkey); + if (!info || info.relName !== relName) continue; + return [ + { + kind: "hydration.removed", + data: { + parentUri: info.parentUri, + relation: relName, + childRkey: event.payload.rkey, + childDid: event.payload.authorDid + } + } + ]; + } + return []; +} + +export function applyEnvelopesToSubscriber( + subscriber: SubscriberView, + envs: TranslatedEnvelope[] +): void { + for (const msg of envs) { + if (msg.kind === "record.created") { + ensureParentSet(subscriber).add(msg.data.record.uri); + } else if (msg.kind === "record.deleted") { + const set = subscriber.parentUris; + if (set instanceof Set) set.delete(msg.data.uri); + else if (Array.isArray(set)) { + const idx = set.indexOf(msg.data.uri); + if (idx >= 0) set.splice(idx, 1); + } + } else if (msg.kind === "hydration.added") { + ensureChildMap(subscriber).set(msg.data.child.rkey, { + parentUri: msg.data.parentUri, + relName: msg.data.relation + }); + } else if (msg.kind === "hydration.removed") { + const map = subscriber.childToParent; + if (map instanceof Map) map.delete(msg.data.childRkey); + else if (map && typeof map === "object") { + delete (map as Record)[msg.data.childRkey]; + } + } + } +} + +function ensureParentSet(sub: SubscriberView): Set { + if (sub.parentUris instanceof Set) return sub.parentUris; + const set = new Set(sub.parentUris ?? []); + sub.parentUris = set; + return set; +} + +function ensureChildMap( + sub: SubscriberView +): Map { + if (sub.childToParent instanceof Map) return sub.childToParent; + const map = new Map(); + if (sub.childToParent && typeof sub.childToParent === "object") { + for (const [k, v] of Object.entries(sub.childToParent)) map.set(k, v); + } + sub.childToParent = map; + return map; +} + +function hasParent( + parents: Set | string[] | undefined, + uri: string +): boolean { + if (!parents) return false; + if (parents instanceof Set) return parents.has(uri); + return parents.includes(uri); +} + +function getChildInfo( + map: + | Map + | Record + | undefined, + rkey: string +): { parentUri: string; relName: string } | undefined { + if (!map) return undefined; + if (map instanceof Map) return map.get(rkey); + return map[rkey]; +} + +function getNestedValue( + obj: Record, + path: string +): unknown { + let cur: unknown = obj; + for (const key of path.split(".")) { + if (cur == null || typeof cur !== "object") return undefined; + cur = (cur as Record)[key]; + } + return cur; +} diff --git a/src/core/realtime/ticket.ts b/src/core/realtime/ticket.ts index 72d0595..7e0fd6a 100644 --- a/src/core/realtime/ticket.ts +++ b/src/core/realtime/ticket.ts @@ -18,6 +18,16 @@ export interface TicketPayload { exp: number; /** Unix ms — useful for debugging; ignored on verify. */ iat: number; + /** Optional: query-scoped watchRecords spec this ticket authorizes. Present + * when the ticket was minted from a watchRecords handshake. The server + * trusts the signed spec on upgrade and forwards it to the DO. */ + querySpec?: TicketQuerySpec; +} + +export interface TicketQuerySpec { + collection: string; + spaceUri: string; + hydrate?: Record; } function normalizeSecret(secret: Uint8Array | string): Uint8Array { @@ -107,13 +117,19 @@ export class TicketSigner { ); } - async sign(input: { topics: string[]; did: string; ttlMs: number }): Promise { + async sign(input: { + topics: string[]; + did: string; + ttlMs: number; + querySpec?: TicketQuerySpec; + }): Promise { const now = Date.now(); const payload: TicketPayload = { topics: input.topics, did: input.did, exp: now + input.ttlMs, iat: now, + ...(input.querySpec ? { querySpec: input.querySpec } : {}), }; const payloadPart = b64urlFromString(JSON.stringify(payload)); const sig = await crypto.subtle.sign( diff --git a/src/core/router/collection.ts b/src/core/router/collection.ts index 9a9a583..5c637c8 100644 --- a/src/core/router/collection.ts +++ b/src/core/router/collection.ts @@ -21,6 +21,210 @@ import { checkAccess } from "../spaces/acl"; import { hashInviteToken } from "../spaces/invite-token"; import type { SpacesContext } from "."; import type { Nsid } from "@atcute/lexicons"; +import type { RealtimeEvent } from "../realtime/types"; +import { sseResponse } from "../realtime/sse"; +import { spaceTopic } from "../realtime/types"; +import type { SubscriberQuerySpec } from "../realtime/durable-object"; +import { DurableObjectPubSub } from "../realtime/durable-object"; +import { TicketSigner, type TicketQuerySpec } from "../realtime/ticket"; +import { parseHydrateParams } from "./hydrate"; +import { getRelationField, getNestedValue, nsidForShortName } from "../types"; + +/** Shared implementation of the watchRecords snapshot+live loop. Called by + * both transport branches (SSE and Worker-terminated WS). The caller owns + * the actual socket/stream and provides a `send(kind, data)` closure. */ +async function runQueryStream(opts: { + send: (kind: string, data: unknown) => void; + abort: AbortController; + spaceUri: string; + callerDid: string | undefined; + params: URLSearchParams; + db: Database; + config: ContrailConfig; + collection: string; + colNsid: string; + pubsub: import("../realtime/types").PubSub; + relations: Record; + references: Record; + childCollectionMap: Map< + string, + { relName: string; matchField: string; matchMode: "uri" | "did" } + >; +}): Promise { + const { + send, + abort, + spaceUri, + callerDid, + params, + db, + config, + collection, + colNsid, + pubsub, + relations, + references, + childCollectionMap + } = opts; + + const hydrateSpec = parseHydrateParams(params, relations, references); + const trackHydration = Object.keys(hydrateSpec.relations).length > 0; + const parentUris = new Set(); + const parentDids = new Set(); + const childToParent = new Map(); + + const primaryUri = (payload: { authorDid: string; collection: string; rkey: string }) => + `at://${payload.authorDid}/${payload.collection}/${payload.rkey}`; + + const handleChildEvent = (event: RealtimeEvent) => { + if (!trackHydration) return; + if (event.kind !== "record.created" && event.kind !== "record.deleted") return; + const meta = childCollectionMap.get(event.payload.collection); + if (!meta) return; + if (!(hydrateSpec.relations as Record)[meta.relName]) return; + if (event.payload.spaceUri !== spaceUri) return; + + if (event.kind === "record.created") { + const matched = getNestedValue(event.payload.record, meta.matchField); + if (matched == null) return; + const parent = + meta.matchMode === "did" + ? parentDids.has(String(matched)) + ? `at://${String(matched)}/${colNsid}/_` + : null + : parentUris.has(String(matched)) + ? String(matched) + : null; + if (!parent) return; + childToParent.set(event.payload.rkey, { + parentUri: parent, + relName: meta.relName + }); + send("hydration.added", { + parentUri: parent, + relation: meta.relName, + child: { + uri: primaryUri(event.payload), + did: event.payload.authorDid, + rkey: event.payload.rkey, + collection: event.payload.collection, + cid: event.payload.cid, + record: event.payload.record, + _space: spaceUri + } + }); + } else { + const info = childToParent.get(event.payload.rkey); + if (!info) return; + childToParent.delete(event.payload.rkey); + send("hydration.removed", { + parentUri: info.parentUri, + relation: info.relName, + childRkey: event.payload.rkey, + childDid: event.payload.authorDid + }); + } + }; + + const handleLive = (event: RealtimeEvent) => { + if (abort.signal.aborted) return; + if (event.kind === "member.removed" && event.payload.did === callerDid) { + send("member.removed", event.payload); + abort.abort(); + return; + } + if (event.kind !== "record.created" && event.kind !== "record.deleted") return; + if (event.payload.spaceUri !== spaceUri) return; + + if (event.payload.collection !== colNsid) { + handleChildEvent(event); + return; + } + + const nowUs = event.ts * 1000; + const uri = primaryUri(event.payload); + if (event.kind === "record.created") { + parentUris.add(uri); + parentDids.add(event.payload.authorDid); + send("record.created", { + record: { + uri, + did: event.payload.authorDid, + rkey: event.payload.rkey, + collection: event.payload.collection, + cid: event.payload.cid, + record: event.payload.record, + time_us: nowUs, + indexed_at: event.ts, + _space: spaceUri + } + }); + } else { + parentUris.delete(uri); + send("record.deleted", { + uri, + did: event.payload.authorDid, + rkey: event.payload.rkey + }); + } + }; + + const topic = spaceTopic(spaceUri); + const iter = pubsub.subscribe(topic, abort.signal); + + const buffered: RealtimeEvent[] = []; + let snapshotDone = false; + + const pump = (async () => { + try { + for await (const event of iter) { + if (abort.signal.aborted) break; + if (!snapshotDone) buffered.push(event); + else handleLive(event); + } + } catch { + /* aborted or errored */ + } + })(); + + try { + send("snapshot.start", { spaceUri, collection: colNsid }); + const result = await runPipeline(db, config, collection, params, undefined, [spaceUri]); + for (const record of result.records) { + if (abort.signal.aborted) break; + if (typeof record.uri === "string") parentUris.add(record.uri); + if (typeof record.did === "string") parentDids.add(record.did); + for (const [relName] of Object.entries(hydrateSpec.relations)) { + const hydratedGroups = (record as Record)[relName]; + if (!hydratedGroups) continue; + const flat: Array<{ rkey?: string }> = Array.isArray(hydratedGroups) + ? (hydratedGroups as Array<{ rkey?: string }>) + : (Object.values(hydratedGroups as Record).flat() as Array<{ + rkey?: string; + }>); + for (const child of flat) { + if (child?.rkey) { + childToParent.set(child.rkey, { + parentUri: record.uri as string, + relName + }); + } + } + } + send("snapshot.record", { record }); + } + send("snapshot.end", { cursor: result.cursor }); + snapshotDone = true; + for (const event of buffered) handleLive(event); + } catch (err) { + send("error", { + message: err instanceof Error ? err.message : String(err) + }); + abort.abort(); + } + + await pump.catch(() => {}); +} export async function runPipeline( db: Database, @@ -194,9 +398,11 @@ export function registerCollectionRoutes( app: Hono, db: Database, config: ContrailConfig, - spacesCtx?: SpacesContext | null + spacesCtx?: SpacesContext | null, + options: { pubsub?: import("../realtime/types").PubSub | null } = {} ): void { const ns = config.namespace; + const pubsub = options.pubsub ?? null; /** When a per-collection endpoint receives `?spaceUri=...`, verify the JWT, * resolve membership, run the space ACL, and return the caller DID if allowed. @@ -242,7 +448,9 @@ export function registerCollectionRoutes( } const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; - const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid, { + authOverride: config.spaces?.authOverride, + }); if (!auth) { return c.json( { error: "AuthRequired", message: "spaceUri requires a valid service-auth JWT or read-grant invite token" }, @@ -277,33 +485,45 @@ export function registerCollectionRoutes( if (spaceUri) { const gated = await gateSpaceAccess(c, spaceUri, "read"); if (gated instanceof Response) return gated; - // ACL passed — dispatch directly to the spaces adapter. - const nsid = colConfig.collection; - const list = await spacesCtx!.adapter.listRecords(spaceUri, nsid, { - byUser: params.get("byUser") ?? undefined, - cursor: params.get("cursor") ?? undefined, - limit: params.get("limit") ? Number(params.get("limit")) : undefined, - }); - return c.json(list); + // Route through runPipeline with a single-element space list so the + // full filter / sort / hydrate / reference surface works on per-space + // queries too, not just on the cross-space union path. + try { + const result = await runPipeline(db, config, collection, params, undefined, [spaceUri]); + return c.json(result); + } catch (e: any) { + if (e.message === "Could not resolve actor") { + return c.json({ error: e.message }, 400); + } + throw e; + } } - // Union path: when Authorization is present, verify the JWT and fold - // in records from spaces the caller is a member of. + // Union path: when Authorization is present (or the dev-mode override + // can supply claims without one), verify and fold in records from + // spaces the caller is a member of. let spaceUris: string[] | undefined; - if (spacesCtx && c.req.header("Authorization")) { + const hasAuthHeader = !!c.req.header("Authorization"); + const hasOverride = !!config.spaces?.authOverride; + if (spacesCtx && (hasAuthHeader || hasOverride)) { const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; - const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); - if (!auth) { + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid, { + authOverride: config.spaces?.authOverride, + }); + if (auth) { + const { spaces } = await spacesCtx.adapter.listSpaces({ + memberDid: auth.issuer, + limit: 200, + }); + spaceUris = spaces.map((s) => s.uri); + } else if (hasAuthHeader) { + // Had an auth header but it was invalid — reject rather than + // silently downgrading to public results. return c.json( { error: "AuthRequired", message: "invalid service-auth JWT" }, 401 ); } - const { spaces } = await spacesCtx.adapter.listSpaces({ - memberDid: auth.issuer, - limit: 200, - }); - spaceUris = spaces.map((s) => s.uri); } try { @@ -316,6 +536,309 @@ export function registerCollectionRoutes( throw e; } }); + + // Streaming variant — same query shape, SSE'd forever. Opted in via the + // presence of the realtime module; no explicit method config needed. + if (pubsub && spacesCtx) { + const colNsid = colConfig.collection; + const relations = colConfig.relations ?? {}; + const references = colConfig.references ?? {}; + // Map child-NSID → { relName, matchField } so we can route child + // events to hydration deltas without re-parsing config per event. + const childCollectionMap = new Map< + string, + { relName: string; matchField: string; matchMode: "uri" | "did" } + >(); + for (const [relName, rel] of Object.entries(relations)) { + const childNsid = nsidForShortName(config, rel.collection) ?? rel.collection; + childCollectionMap.set(childNsid, { + relName, + matchField: getRelationField(rel), + matchMode: rel.match ?? "uri" + }); + } + + // TicketSigner for watch-scoped tickets. Minted on `mode=ws` handshake + // so the subsequent WS upgrade can auth with just `?ticket=...` (no + // cookie or JWT needed — enables cross-origin + stateless clients). + const ticketSigner = config.realtime?.ticketSecret + ? new TicketSigner(config.realtime.ticketSecret) + : null; + const ticketTtl = config.realtime?.ticketTtlMs ?? 120_000; + + app.get(`/xrpc/${ns}.${collection}.watchRecords`, async (c) => { + const params = new URL(c.req.url).searchParams; + const spaceUri = params.get("spaceUri"); + if (!spaceUri) { + return c.json( + { error: "InvalidRequest", message: "spaceUri required (cross-space watch is deferred)" }, + 400 + ); + } + + // Try ticket-auth first: if a valid watch ticket scoped to this + // spaceUri/collection is present, use it and skip the JWT gate. + let callerDid: string | undefined; + let querySpec: SubscriberQuerySpec; + let ticketSpec: SubscriberQuerySpec | null = null; + + const providedTicket = params.get("ticket"); + if (providedTicket && ticketSigner) { + const payload = await ticketSigner.verify(providedTicket); + if (payload?.querySpec) { + const ts = payload.querySpec; + if ( + ts.collection === colNsid && + ts.spaceUri === spaceUri && + payload.topics.includes(spaceTopic(spaceUri)) + ) { + callerDid = payload.did; + ticketSpec = { + collection: ts.collection, + spaceUri: ts.spaceUri, + ...(ts.hydrate ? { hydrate: ts.hydrate } : {}) + }; + } + } + } + + if (!ticketSpec) { + const gated = await gateSpaceAccess(c, spaceUri, "read"); + if (gated instanceof Response) return gated; + callerDid = "callerDid" in gated ? gated.callerDid : undefined; + } + + // Build the query spec the DO will filter events against. Prefer + // the ticket's spec when present (guarantees parity with what the + // client asked for at handshake time, no param drift). + const hydrateSpec = parseHydrateParams(params, relations, references); + querySpec = ticketSpec ?? { + collection: colNsid, + spaceUri, + ...(Object.keys(hydrateSpec.relations).length > 0 + ? { + hydrate: Object.fromEntries( + Object.entries(hydrateSpec.relations).map(([relName]) => { + const rel = relations[relName]!; + const childNsid = + nsidForShortName(config, rel.collection) ?? rel.collection; + return [ + relName, + { childCollection: childNsid, matchField: getRelationField(rel) } + ]; + }) + ) + } + : {}) + }; + + // Upgrade-to-WS path — forward directly to the DO with the spec, + // so the DO terminates the socket and hibernates when idle. + // Requires snapshot to be fetched separately (see `mode=ws` JSON + // handshake below) or accepted as lossy-on-connect for a plain WS + // upgrade. + const isUpgrade = c.req.header("Upgrade")?.toLowerCase() === "websocket"; + const isWsMode = params.get("mode") === "ws"; + + if (isWsMode && !isUpgrade) { + // Handshake: return snapshot + a ticket the client uses to + // upgrade. Ticket carries the (did, topic, querySpec) signed + // so the WS-upgrade route skips any other auth. + try { + // Capture a server-side timestamp BEFORE running the snapshot. + // Any event published after this moment will have ts > sinceTs + // and be replayed by the DO on WS connect — so the client + // never misses events during the snapshot→WS gap. + const sinceTs = Date.now(); + const result = await runPipeline( + db, + config, + collection, + params, + undefined, + [spaceUri] + ); + let ticket: string | undefined; + if (ticketSigner && callerDid) { + ticket = await ticketSigner.sign({ + topics: [spaceTopic(spaceUri)], + did: callerDid, + ttlMs: ticketTtl, + querySpec: querySpec as TicketQuerySpec + }); + } + const wsUrl = (() => { + const u = new URL(c.req.url); + u.searchParams.delete("mode"); + if (ticket) u.searchParams.set("ticket", ticket); + u.searchParams.set("sinceTs", String(sinceTs)); + return u.pathname + u.search; + })(); + return c.json({ + transport: "ws", + snapshot: { records: result.records, cursor: result.cursor }, + querySpec, + ticket, + ticketTtlMs: ticketTtl, + sinceTs, + wsUrl + }); + } catch (err) { + return c.json( + { error: "SnapshotFailed", message: err instanceof Error ? err.message : String(err) }, + 500 + ); + } + } + + if (isUpgrade && pubsub instanceof DurableObjectPubSub) { + // Forward the WS upgrade to the DO. The DO owns the socket from + // here and hibernates when idle. Replays any events buffered + // since the handshake `sinceTs` so the client closes the gap. + const sinceTsParam = params.get("sinceTs"); + const sinceTs = sinceTsParam ? Number(sinceTsParam) : 0; + return pubsub.forwardSubscribe(spaceTopic(spaceUri), c.req.raw, { + did: callerDid, + querySpec, + sinceTs: Number.isFinite(sinceTs) ? sinceTs : 0 + }); + } + + const ac = new AbortController(); + const reqSignal = c.req.raw.signal; + if (reqSignal) { + if (reqSignal.aborted) ac.abort(); + else reqSignal.addEventListener("abort", () => ac.abort(), { once: true }); + } + + // Worker-terminated WebSocket — used when pubsub isn't DO-backed + // (dev InMemoryPubSub). Same query-filter loop as SSE; different + // transport. Runs in the same isolate so no cost benefit, but + // matches the prod protocol. + if (isUpgrade) { + const WsPair = (globalThis as unknown as { WebSocketPair?: any }).WebSocketPair; + if (!WsPair) { + return c.json( + { error: "NotSupported", reason: "websockets-require-workers-runtime" }, + 426 + ); + } + const pair = new WsPair(); + const clientWs = pair[0] as WebSocket; + const serverWs = pair[1] as WebSocket & { accept?: () => void }; + serverWs.accept?.(); + + const sendWs = (kind: string, data: unknown) => { + try { + serverWs.send(JSON.stringify({ kind, data })); + } catch { + ac.abort(); + } + }; + serverWs.addEventListener?.("close", () => ac.abort()); + serverWs.addEventListener?.("error", () => ac.abort()); + + void runQueryStream({ + send: sendWs, + abort: ac, + spaceUri, + callerDid, + params, + db, + config, + collection, + colNsid, + pubsub, + relations, + references, + childCollectionMap + }).finally(() => { + try { + serverWs.close(); + } catch { + /* ignore */ + } + }); + + return new Response(null, { + status: 101, + webSocket: clientWs + } as ResponseInit & { webSocket: unknown }); + } + + // SSE fallback. + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + let closed = false; + const close = () => { + if (closed) return; + closed = true; + try { + controller.close(); + } catch { + /* already closed */ + } + }; + ac.signal.addEventListener("abort", close, { once: true }); + + const send = (kind: string, data: unknown) => { + if (closed) return; + try { + controller.enqueue( + encoder.encode(`event: ${kind}\ndata: ${JSON.stringify(data)}\n\n`) + ); + } catch { + close(); + } + }; + + const keepalive = setInterval(() => { + if (closed) return; + try { + controller.enqueue(encoder.encode(`: keepalive\n\n`)); + } catch { + close(); + } + }, 15_000); + ac.signal.addEventListener( + "abort", + () => clearInterval(keepalive), + { once: true } + ); + + void runQueryStream({ + send, + abort: ac, + spaceUri, + callerDid, + params, + db, + config, + collection, + colNsid, + pubsub, + relations, + references, + childCollectionMap + }).finally(() => close()); + }, + cancel() { + ac.abort(); + }, + }); + + return new Response(stream, { + status: 200, + headers: { + "content-type": "text/event-stream", + "cache-control": "no-cache, no-transform", + connection: "keep-alive", + "x-accel-buffering": "no", + }, + }); + }); + } } if (!methods.includes("getRecord")) { diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 7baaa32..35ef535 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -108,17 +108,19 @@ export function createApp( } registerAdminRoutes(app, db, config); - registerCollectionRoutes(app, db, config, spacesCtx); + registerCollectionRoutes(app, db, config, spacesCtx, { pubsub: realtimePubsub }); registerFeedRoutes(app, db, config); registerNotifyRoute(app, db, config); registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx); + const authOverride = config.spaces?.authOverride; + if (config.community && spacesCtx) { // Community routes reuse the spaces service-auth middleware (same JWT verifier). const authMiddleware = options.community?.authMiddleware ?? options.spaces?.authMiddleware ?? - createServiceAuthMiddleware(spacesCtx.verifier); + createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); registerCommunityRoutes( app, spacesDb, @@ -132,7 +134,7 @@ export function createApp( const authMiddleware = options.realtime?.authMiddleware ?? options.spaces?.authMiddleware ?? - createServiceAuthMiddleware(spacesCtx.verifier); + createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; registerRealtimeRoutes(app, config, spacesCtx.adapter, communityAdapter, { authMiddleware, diff --git a/src/core/spaces/auth.ts b/src/core/spaces/auth.ts index d1b92fd..8a1eb89 100644 --- a/src/core/spaces/auth.ts +++ b/src/core/spaces/auth.ts @@ -43,17 +43,43 @@ export interface ServiceAuthOptions { /** Hono middleware that verifies the Authorization: Bearer as an atproto * service-auth token. On success, attaches the decoded claims to c.var.serviceAuth. - * Expected Nsid method is taken from the route pattern (last segment after /xrpc/). */ + * Expected Nsid method is taken from the route pattern (last segment after /xrpc/). + * + * If `authOverride` is provided and returns non-null claims, the JWT check is + * skipped and the override's claims are attached instead — used to paper over + * bsky.social's loopback-client restriction on `getServiceAuth` during dev. + * The override must be gated in the caller (e.g. an env flag) since it + * bypasses cryptographic verification. */ export function createServiceAuthMiddleware( - verifier: ServiceJwtVerifier + verifier: ServiceJwtVerifier, + options: { + authOverride?: ( + req: Request + ) => Promise | import("./types").AuthOverrideResult | null; + } = {} ): MiddlewareHandler { return async (c, next) => { + const lxm = extractLxmFromPath(c); + + if (options.authOverride) { + const override = await options.authOverride(c.req.raw); + if (override) { + c.set("serviceAuth", { + issuer: override.issuer, + audience: override.audience ?? "", + lxm: override.lxm ?? lxm ?? undefined, + clientId: override.clientId, + } satisfies ServiceAuth); + await next(); + return; + } + } + const header = c.req.header("Authorization"); if (!header || !header.startsWith("Bearer ")) { return c.json({ error: "AuthRequired", message: "Missing bearer token" }, 401); } const token = header.slice(7).trim(); - const lxm = extractLxmFromPath(c); const result = await verifier.verify(token, { lxm }); if (!result.ok) { @@ -85,12 +111,32 @@ export function requireServiceAuth(c: Context): ServiceAuth { /** Verify a request's Authorization: Bearer token out-of-band (e.g. from a * route handler that doesn't always require auth). Returns the claims on - * success, or null if missing/invalid. */ + * success, or null if missing/invalid. + * + * If `authOverride` is provided and returns claims, the JWT path is skipped + * and those claims are returned — same bypass as the middleware-level auth. */ export async function verifyServiceAuthRequest( verifier: ServiceJwtVerifier, request: Request, - lxm?: Nsid | null + lxm?: Nsid | null, + options: { + authOverride?: ( + req: Request + ) => Promise | import("./types").AuthOverrideResult | null; + } = {} ): Promise { + if (options.authOverride) { + const override = await options.authOverride(request); + if (override) { + return { + issuer: override.issuer, + audience: override.audience ?? "", + lxm: override.lxm ?? lxm ?? undefined, + clientId: override.clientId, + }; + } + } + const header = request.headers.get("Authorization"); if (!header || !header.startsWith("Bearer ")) return null; const token = header.slice(7).trim(); diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index 286c81f..6027d06 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -46,7 +46,10 @@ export function registerSpacesRoutes( const adapter = options.adapter ?? ctx?.adapter ?? new HostedAdapter(db, config); const verifier = ctx?.verifier ?? buildVerifier(spacesConfig); const auth = - options.authMiddleware ?? createServiceAuthMiddleware(verifier); + options.authMiddleware ?? + createServiceAuthMiddleware(verifier, { + authOverride: spacesConfig.authOverride, + }); /** Read-route auth: skip the JWT middleware when an `?inviteToken=` is * present so anonymous bearer reads don't 401 before the route handler can diff --git a/src/core/spaces/types.ts b/src/core/spaces/types.ts index 397e6a8..8ba8374 100644 --- a/src/core/spaces/types.ts +++ b/src/core/spaces/types.ts @@ -37,6 +37,24 @@ export interface SpacesConfig { resolver?: DidDocumentResolver; /** Blob-upload backend. When omitted, blob XRPCs are not exposed. */ blobs?: SpacesBlobsConfig; + /** Dev-only auth bypass. Runs BEFORE JWT verification. If it returns + * non-null claims, those are used as the authenticated caller and the + * JWT check is skipped. Intended for local development where bsky.social + * rejects `getServiceAuth` for loopback OAuth clients; pair with a + * signed-cookie session check so only your own browser can trigger it. + * Never set in production. */ + authOverride?: ( + req: Request + ) => Promise | AuthOverrideResult | null; +} + +/** Claims returned by an `authOverride`. Matches the ServiceAuth shape the + * normal JWT path attaches to the request. */ +export interface AuthOverrideResult { + issuer: string; + audience?: string; + lxm?: string; + clientId?: string; } export interface SpaceRow { diff --git a/src/generate.ts b/src/generate.ts index c56fa6b..c68ea87 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -690,6 +690,40 @@ export function generateLexicons(options: GenerateOptions): Record..watchRecords` + * endpoint and maintains a keyed, reactive-ish record store. The core is + * framework-agnostic — you get a subscribable store; wrap it in your + * framework's reactive primitives. + * + * Two transports: + * - `sse` (default) — opens an EventSource against `url`. Simplest. + * - `ws` — two-step: HTTP GET to `url&mode=ws` for the snapshot + wsUrl, + * then WebSocket upgrade. On Cloudflare this routes the live stream + * through a Durable Object with hibernation, so idle connections cost + * near-zero at scale. */ + +export interface WatchRecord { + uri: string; + did: string; + rkey: string; + collection: string; + record: Record; + time_us?: number; + indexed_at?: number; + cid?: string | null; + /** Set when the record originates from a per-space table. */ + _space?: string; + /** Additional hydrated relations / references populated server-side. */ + [k: string]: unknown; +} + +export interface WatchStoreOptions { + /** Fully-qualified URL of the `watchRecords` endpoint, including query params. */ + url: string; + /** Transport. Default: 'sse'. Use 'ws' on Cloudflare for DO-terminated + * long-lived subscriptions that hibernate while idle. */ + transport?: "sse" | "ws"; + /** Optional: provide a ticket string (for `?ticket=...`-style auth). + * When omitted, the connection is opened without an explicit token — + * rely on cookies / bearer tokens your fetch policy sends automatically. */ + fetchTicket?: () => Promise; + /** Custom compare. Default: sort by `time_us` descending (newest first), + * tie-breaking by rkey. */ + compareRecords?: (a: WatchRecord, b: WatchRecord) => number; + /** Reconnect on error with backoff. Default: true, 1s → 30s exponential. */ + reconnect?: boolean; + /** Optional logger for debug output. */ + logger?: { + log?: (...args: unknown[]) => void; + warn?: (...args: unknown[]) => void; + error?: (...args: unknown[]) => void; + }; +} + +export type WatchStoreStatus = + | "idle" + | "connecting" + | "snapshot" + | "live" + | "reconnecting" + | "closed"; + +export interface WatchStore { + readonly records: ReadonlyArray; + readonly status: WatchStoreStatus; + readonly error: Error | null; + /** Subscribe to changes. Called once with the current state on subscribe. */ + subscribe(listener: (state: WatchStoreState) => void): () => void; + /** Open the connection and begin consuming. Safe to call multiple times. */ + start(): void; + /** Close the connection and clear the store. */ + stop(): void; +} + +export interface WatchStoreState { + records: ReadonlyArray; + status: WatchStoreStatus; + error: Error | null; +} + +// --------------------------------------------------------------------------- + +const defaultCompare = (a: WatchRecord, b: WatchRecord): number => { + const at = a.time_us ?? 0; + const bt = b.time_us ?? 0; + if (at !== bt) return bt - at; + return a.rkey < b.rkey ? 1 : a.rkey > b.rkey ? -1 : 0; +}; + +export function createWatchStore(options: WatchStoreOptions): WatchStore { + const compare = options.compareRecords ?? defaultCompare; + const reconnect = options.reconnect !== false; + const transport = options.transport ?? "sse"; + const log = options.logger ?? {}; + + const byKey = new Map(); + let sorted: WatchRecord[] = []; + let status: WatchStoreStatus = "idle"; + let error: Error | null = null; + const listeners = new Set<(state: WatchStoreState) => void>(); + + let es: EventSource | null = null; + let ws: WebSocket | null = null; + let started = false; + let stopped = false; + let backoffMs = 1000; + + const stateSnapshot = (): WatchStoreState => ({ records: sorted, status, error }); + + const notify = () => { + const s = stateSnapshot(); + for (const l of listeners) l(s); + }; + + const resort = () => { + sorted = Array.from(byKey.values()).sort(compare); + }; + + const setStatus = (next: WatchStoreStatus, nextError: Error | null = null) => { + status = next; + error = nextError; + notify(); + }; + + const key = (r: { did?: string; rkey?: string; uri?: string }): string => { + if (r.uri) return r.uri; + if (r.did && r.rkey) return `${r.did}/${r.rkey}`; + throw new Error("record must have uri or did+rkey"); + }; + + const applySnapshotRecord = (record: WatchRecord) => { + byKey.set(key(record), record); + }; + + const applyCreated = (record: WatchRecord) => { + byKey.set(key(record), record); + resort(); + notify(); + }; + + const applyDeleted = (info: { uri?: string; did?: string; rkey?: string }) => { + const k = key(info); + if (!byKey.delete(k)) return; + resort(); + notify(); + }; + + const applyHydrationAdded = ( + parentUri: string, + relation: string, + child: WatchRecord + ) => { + const parent = byKey.get(parentUri); + if (!parent) return; + const existing = + ((parent as Record)[relation] as WatchRecord[] | undefined) ?? []; + if (existing.some((c) => c.rkey === child.rkey)) return; + (parent as Record)[relation] = [...existing, child]; + resort(); + notify(); + }; + + const applyHydrationRemoved = ( + parentUri: string, + relation: string, + childRkey: string + ) => { + const parent = byKey.get(parentUri); + if (!parent) return; + const existing = (parent as Record)[relation] as + | WatchRecord[] + | undefined; + if (!existing) return; + const filtered = existing.filter((c) => c.rkey !== childRkey); + if (filtered.length === existing.length) return; + (parent as Record)[relation] = filtered; + resort(); + notify(); + }; + + // Dispatch a single decoded event envelope to the store. Shared by both + // transports — SSE event handlers and WS message handlers both funnel here. + type IncomingMessage = + | { kind: "snapshot.start"; data: unknown } + | { kind: "snapshot.record"; data: { record: WatchRecord } } + | { kind: "snapshot.end"; data: unknown } + | { kind: "record.created"; data: { record: WatchRecord } } + | { kind: "record.deleted"; data: { uri?: string; did?: string; rkey?: string } } + | { + kind: "hydration.added"; + data: { parentUri: string; relation: string; child: WatchRecord }; + } + | { + kind: "hydration.removed"; + data: { parentUri: string; relation: string; childRkey: string }; + } + | { kind: "member.removed"; data: unknown }; + + const handleMessage = (msg: IncomingMessage) => { + switch (msg.kind) { + case "snapshot.start": + setStatus("snapshot"); + backoffMs = 1000; + break; + case "snapshot.record": + applySnapshotRecord(msg.data.record); + break; + case "snapshot.end": + resort(); + setStatus("live"); + break; + case "record.created": + applyCreated(msg.data.record); + break; + case "record.deleted": + applyDeleted(msg.data); + break; + case "hydration.added": + applyHydrationAdded(msg.data.parentUri, msg.data.relation, msg.data.child); + break; + case "hydration.removed": + applyHydrationRemoved(msg.data.parentUri, msg.data.relation, msg.data.childRkey); + break; + case "member.removed": + byKey.clear(); + sorted = []; + setStatus("closed"); + closeConnections(); + break; + } + }; + + const closeConnections = () => { + if (es) { + es.close(); + es = null; + } + if (ws) { + try { + ws.close(); + } catch { + /* ignore */ + } + ws = null; + } + }; + + const openSse = async () => { + let url = options.url; + if (options.fetchTicket) { + const ticket = await options.fetchTicket(); + const sep = url.includes("?") ? "&" : "?"; + url += `${sep}ticket=${encodeURIComponent(ticket)}`; + } + const source = new EventSource(url); + es = source; + + const on = (kind: IncomingMessage["kind"]) => + source.addEventListener(kind, (e) => { + try { + const data = JSON.parse((e as MessageEvent).data); + handleMessage({ kind, data } as IncomingMessage); + } catch (err) { + log.warn?.(`${kind} parse failed`, err); + } + }); + + on("snapshot.start"); + on("snapshot.record"); + on("snapshot.end"); + on("record.created"); + on("record.deleted"); + on("hydration.added"); + on("hydration.removed"); + on("member.removed"); + + source.addEventListener("error", () => { + if (source.readyState === EventSource.CLOSED) { + if (stopped) return; + setStatus("reconnecting", new Error("stream closed")); + scheduleReconnect(); + } + }); + }; + + const openWs = async () => { + // Step 1: snapshot + handshake. + let snapshotUrl = options.url; + const sep = snapshotUrl.includes("?") ? "&" : "?"; + snapshotUrl += `${sep}mode=ws`; + if (options.fetchTicket) { + const ticket = await options.fetchTicket(); + snapshotUrl += `&ticket=${encodeURIComponent(ticket)}`; + } + const res = await fetch(snapshotUrl, { headers: { accept: "application/json" } }); + if (!res.ok) throw new Error(`snapshot fetch failed (${res.status})`); + const handshake = (await res.json()) as { + transport: "ws"; + snapshot: { records: WatchRecord[]; cursor?: string }; + wsUrl: string; + /** Watch-scoped ticket the server embeds in `wsUrl` for WS auth. + * Opaque to the client; passed back verbatim on upgrade. */ + ticket?: string; + /** Unix ms captured by the server before the snapshot ran. The + * server embeds this in wsUrl as `?sinceTs=...` so the DO can + * replay events from the race window on connect. */ + sinceTs?: number; + }; + + // Apply snapshot in-order. + setStatus("snapshot"); + for (const record of handshake.snapshot.records) applySnapshotRecord(record); + resort(); + backoffMs = 1000; + + // Step 2: open the WS. `wsUrl` is a relative path from the server. + const wsHref = new URL(handshake.wsUrl, options.url); + wsHref.protocol = wsHref.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket(wsHref.toString()); + ws = socket; + + socket.addEventListener("open", () => { + setStatus("live"); + }); + + socket.addEventListener("message", (e) => { + try { + const parsed = JSON.parse((e as MessageEvent).data as string); + if (!parsed || typeof parsed !== "object" || !parsed.kind) return; + handleMessage(parsed as IncomingMessage); + } catch (err) { + log.warn?.("ws message parse failed", err); + } + }); + + socket.addEventListener("close", () => { + if (stopped) return; + setStatus("reconnecting", new Error("ws closed")); + scheduleReconnect(); + }); + + socket.addEventListener("error", (err) => { + log.warn?.("ws error", err); + }); + }; + + const openOnce = async () => { + setStatus("connecting"); + try { + if (transport === "ws") await openWs(); + else await openSse(); + } catch (err) { + setStatus( + "reconnecting", + err instanceof Error ? err : new Error(String(err)) + ); + scheduleReconnect(); + } + }; + + const scheduleReconnect = () => { + if (!reconnect || stopped) { + setStatus("closed"); + return; + } + const delay = Math.min(backoffMs, 30_000); + backoffMs = Math.min(backoffMs * 2, 30_000); + setTimeout(() => { + if (stopped) return; + byKey.clear(); + sorted = []; + void openOnce(); + }, delay); + }; + + return { + get records() { + return sorted; + }, + get status() { + return status; + }, + get error() { + return error; + }, + subscribe(listener) { + listeners.add(listener); + listener(stateSnapshot()); + return () => listeners.delete(listener); + }, + start() { + if (started) return; + started = true; + stopped = false; + void openOnce(); + }, + stop() { + stopped = true; + closeConnections(); + byKey.clear(); + sorted = []; + setStatus("closed"); + } + }; +} diff --git a/tsup.config.ts b/tsup.config.ts index cffcd4a..743ec36 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ "src/server.ts", "src/generate.ts", "src/publish.ts", + "src/sync/index.ts", "src/adapters/sqlite.ts", "src/adapters/postgres.ts", ], -- 2.51.2 From d190c59c3078ec32cbcb21426cbeb69280ce9154 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:32:52 +0200 Subject: [PATCH 09/18] cleanup pt1 --- app/config.ts | 3 +- .../{ => space}/whoami.json | 2 +- docs/community-spec-mapping.md | 9 +- docs/spaces-later.md | 14 +- docs/spaces-spec-mapping.md | 16 +- docs/todo.md | 4 - .../tools/atmo/chat/channel/watchRecords.json | 114 +++++++++++++ .../chat/community/{ => space}/whoami.json | 2 +- .../tools/atmo/chat/message/watchRecords.json | 104 +++++++++++ .../tools/atmo/chat/permissionSet.json | 15 +- .../tools/atmo/chat/server/watchRecords.json | 109 ++++++++++++ .../{space => spaceExt}/invite/create.json | 2 +- .../chat/{space => spaceExt}/invite/list.json | 2 +- .../{space => spaceExt}/invite/redeem.json | 2 +- .../{space => spaceExt}/invite/revoke.json | 2 +- .../atmo/chat/{space => spaceExt}/whoami.json | 2 +- .../src/lib/atproto/generated-methods.ts | 15 +- .../src/lib/contrail/index.ts | 21 +-- .../src/lib/rooms/connection.svelte.ts | 35 ++++ .../src/lib/rooms/realtime.svelte.ts | 23 ++- .../src/lib/rooms/server.ts | 2 +- .../src/routes/api/watch-token/+server.ts | 25 +++ .../routes/c/[communityDid]/+layout.server.ts | 2 +- .../routes/c/[communityDid]/+layout.svelte | 19 +++ .../[communityDid]/[channelKey]/+page.svelte | 58 +++++-- .../rsvp/atmo/community/defs.json | 50 ++++++ .../rsvp/atmo/community/invite/create.json | 93 ++++++++++ .../rsvp/atmo/community/invite/list.json | 52 ++++++ .../rsvp/atmo/community/invite/redeem.json | 60 +++++++ .../rsvp/atmo/community/invite/revoke.json | 46 +++++ .../atmo/community/{ => space}/whoami.json | 2 +- .../rsvp/atmo/event/getRecord.json | 58 +------ .../rsvp/atmo/event/listRecords.json | 58 +------ lexicons-generated/rsvp/atmo/getProfile.json | 58 +------ .../rsvp/atmo/permissionSet.json | 19 ++- .../rsvp/atmo/rsvp/getRecord.json | 58 +------ .../rsvp/atmo/rsvp/listRecords.json | 58 +------ .../{space => spaceExt}/invite/create.json | 2 +- .../atmo/{space => spaceExt}/invite/list.json | 2 +- .../{space => spaceExt}/invite/redeem.json | 2 +- .../{space => spaceExt}/invite/revoke.json | 2 +- .../rsvp/atmo/{space => spaceExt}/whoami.json | 2 +- lexicons-pulled/README.md | 2 +- .../site/standard/publication.json | 66 ------- .../site/standard/theme/basic.json | 38 ----- .../site/standard/theme/color.json | 54 ------ .../{ => ext}/invite/create.json | 2 +- .../{ => ext}/invite/list.json | 2 +- .../{ => ext}/invite/redeem.json | 2 +- .../{ => ext}/invite/revoke.json | 2 +- .../{ => ext}/whoami.json | 2 +- src/contrail.ts | 60 ++++++- src/core/community/router.ts | 10 +- src/core/db/records.ts | 46 +++++ src/core/invite/index.ts | 1 + .../invite-token.ts => invite/token.ts} | 8 + src/core/jetstream.ts | 5 +- src/core/persistent.ts | 7 +- src/core/realtime/publishing-adapter.ts | 22 ++- src/core/realtime/query-filter.ts | 12 +- src/core/realtime/resolve.ts | 21 ++- src/core/realtime/router.ts | 120 ++++++++----- src/core/realtime/types.ts | 37 +++- src/core/router/collection.ts | 22 ++- src/core/router/index.ts | 44 +++-- src/core/spaces/router.ts | 23 +-- src/core/types.ts | 6 + src/generate.ts | 27 ++- src/index.ts | 2 +- src/server.ts | 31 ++-- src/sync/index.ts | 63 ++++++- tests/community-delegation.test.ts | 2 +- tests/community-e2e.test.ts | 6 +- tests/community-mint.test.ts | 2 +- tests/contrail-entry.test.ts | 95 +++++++++++ tests/realtime-durable-object.test.ts | 11 +- tests/realtime-e2e.test.ts | 2 +- tests/realtime-foundations.test.ts | 7 +- tests/realtime-public.test.ts | 161 ++++++++++++++++++ tests/spaces-invites.test.ts | 46 ++--- 80 files changed, 1560 insertions(+), 733 deletions(-) rename community-lexicon-templates/{ => space}/whoami.json (94%) delete mode 100644 docs/todo.md create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/{ => space}/whoami.json (95%) create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{space => spaceExt}/invite/create.json (97%) rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{space => spaceExt}/invite/list.json (95%) rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{space => spaceExt}/invite/redeem.json (94%) rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{space => spaceExt}/invite/revoke.json (95%) rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{space => spaceExt}/whoami.json (95%) create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts create mode 100644 examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts create mode 100644 lexicons-generated/rsvp/atmo/community/invite/create.json create mode 100644 lexicons-generated/rsvp/atmo/community/invite/list.json create mode 100644 lexicons-generated/rsvp/atmo/community/invite/redeem.json create mode 100644 lexicons-generated/rsvp/atmo/community/invite/revoke.json rename lexicons-generated/rsvp/atmo/community/{ => space}/whoami.json (95%) rename lexicons-generated/rsvp/atmo/{space => spaceExt}/invite/create.json (97%) rename lexicons-generated/rsvp/atmo/{space => spaceExt}/invite/list.json (96%) rename lexicons-generated/rsvp/atmo/{space => spaceExt}/invite/redeem.json (95%) rename lexicons-generated/rsvp/atmo/{space => spaceExt}/invite/revoke.json (95%) rename lexicons-generated/rsvp/atmo/{space => spaceExt}/whoami.json (96%) delete mode 100644 lexicons-pulled/site/standard/publication.json delete mode 100644 lexicons-pulled/site/standard/theme/basic.json delete mode 100644 lexicons-pulled/site/standard/theme/color.json rename spaces-lexicon-templates/{ => ext}/invite/create.json (97%) rename spaces-lexicon-templates/{ => ext}/invite/list.json (95%) rename spaces-lexicon-templates/{ => ext}/invite/redeem.json (94%) rename spaces-lexicon-templates/{ => ext}/invite/revoke.json (94%) rename spaces-lexicon-templates/{ => ext}/whoami.json (95%) create mode 100644 src/core/invite/index.ts rename src/core/{spaces/invite-token.ts => invite/token.ts} (78%) create mode 100644 tests/contrail-entry.test.ts create mode 100644 tests/realtime-public.test.ts diff --git a/app/config.ts b/app/config.ts index d5ff02e..6dd74dc 100644 --- a/app/config.ts +++ b/app/config.ts @@ -60,7 +60,6 @@ export const config: ContrailConfig = { }, profiles: [ - "app.bsky.actor.profile", - { collection: "site.standard.publication", rkey: "blento.self" } + "app.bsky.actor.profile" ] }; diff --git a/community-lexicon-templates/whoami.json b/community-lexicon-templates/space/whoami.json similarity index 94% rename from community-lexicon-templates/whoami.json rename to community-lexicon-templates/space/whoami.json index bf5e847..fd0a6fa 100644 --- a/community-lexicon-templates/whoami.json +++ b/community-lexicon-templates/space/whoami.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.community.whoami", + "id": "tools.atmo.community.space.whoami", "defs": { "main": { "type": "query", diff --git a/docs/community-spec-mapping.md b/docs/community-spec-mapping.md index ba2cc1b..290ee62 100644 --- a/docs/community-spec-mapping.md +++ b/docs/community-spec-mapping.md @@ -21,13 +21,13 @@ permissioned-data spec with no knowledge of access levels or delegation. | ------------------------------------------- | ------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------- | | Community as a DID | `communities.did` | ✅ | 1:1 | | Mint a fresh did:plc for a community | `community.mint` → P-256 keypairs + genesis op + plc.directory POST | ✅ | Post uses secp256k1; we use P-256 (both spec-valid, we avoid the dep) | -| Adopt an existing account | `community.adopt` (app password) | ➕ | Not in the post; contrail addition. See [community.md](./community.md) for the rationale | +| Adopt an existing account | `community.adopt` (app password) | ➕ | Not in the post; contrail addition — app passwords avoid OAuth's periodic re-auth, useful for machine-operated accounts | | Creator-held rotation key (recovery) | Returned once by `community.mint` as `recoveryKey` | ✅ | Never stored; caller must save it | | Groups-are-spaces | Community-owned rows in `spaces` | ✅ | No separate groups table; every group is a space | | `$admin` reserved space | Auto-created on community creation | ✅ | Keyed by the literal string `$admin`; cannot be deleted | | `$publishers` reserved space | Auto-created on community creation | ➕ | Not in the post; contrail extension for the "publish public records as the community" capability | | Delegated membership (space → space) | `community_access_levels.subject_kind = 'space'` | ✅ | Recursive resolution with cycle guard + depth cap | -| Access-level ladder | `member` / `manager` / `admin` / `owner` | ⚠️ | 4 levels vs the post's 8. See [community.md § Relationship to the Arbiter post](./community.md) | +| Access-level ladder | `member` / `manager` / `admin` / `owner` | ⚠️ | 4 levels vs the post's 8 — post-levels 2–3 (add/remove members) collapse into `manager`; levels 4–5 collapse into `admin` | | Read-Member-List (pre-member tier) | _none_ | ❌ | Post's level 1; skipped in v1 | | Add-Members vs Remove-Members split | Bundled into `manager` | ⚠️ | Post treats them separately | | Configure-Space | `admin` in that space | ✅ | Post's level 5 | @@ -35,7 +35,7 @@ permissioned-data spec with no knowledge of access levels or delegation. | Remove-Space | `owner` in target space OR `admin` in `$admin` | ✅ | Post's level 7 | | Owner | `owner`; only meaningful in `$admin` for owner-management | ✅ | Post's level 8 | | Push model for membership lists | Reconciler writes `spaces_members` after each change | ➕ | Post doesn't specify a sync direction; push keeps spaces read-path zero-overhead | -| Cross-community / cross-arbiter delegation | _same-contrail only_ | ❌ | v1 constraint. Private-membership federation is deferred — see [community.md § Deferred work] | +| Cross-community / cross-arbiter delegation | _same-contrail only_ | ❌ | v1 constraint. Private-membership federation is deferred until the cross-instance distribution shape firms up | | Invites as a separate service | `community.invite.*` (create/list/revoke/redeem) | ➕ | Built-in — tokens pre-sign a grant, redeemer gets the encoded access level. SHA-256 at rest, atomic redeem. | | Writing records under the arbiter's account | `community.space.putRecord` (in-space) + `community.putRecord` (public) | ✅ | In-space: `admin+`. Public: `member+` in `$publishers` — routes through adopted community's PDS | | Public membership-list flag | _none_ | ❌ | Post allows spaces to expose membership publicly. Ruled out for cross-instance federation (privacy) | @@ -55,7 +55,7 @@ membership is user-controlled or community-controlled. ### Community lifecycle - `community.adopt` · `community.mint` · `community.reauth` · `community.delete` -- `community.list` · `community.getHealth` · `community.whoami` +- `community.list` · `community.getHealth` ### Space (group / role / channel) lifecycle - `community.space.create` · `community.space.delete` @@ -63,6 +63,7 @@ membership is user-controlled or community-controlled. ### Membership - `community.space.grant` · `community.space.revoke` · `community.space.setAccessLevel` - `community.space.listMembers` (`?flatten=true` for the resolved DID list) · `community.space.resync` +- `community.space.whoami` — caller's effective access level in a space ### Invites - `community.invite.create` · `community.invite.list` · `community.invite.revoke` · `community.invite.redeem` diff --git a/docs/spaces-later.md b/docs/spaces-later.md index 972a4ce..270b55c 100644 --- a/docs/spaces-later.md +++ b/docs/spaces-later.md @@ -61,15 +61,11 @@ don't have that yet. Lightweight interim: Server-Sent Events on `space.subscribeRecords?spaceUri=&collection=`. Works for first-party apps right away; swap to the real thing later. -## Namespace split for contrail-specific extras -Right now `space.invite.*`, `space.whoami`, and `space.leaveSpace` all live -alongside spec-adjacent endpoints. If the spec lands with different names or -semantics for some of these, migration cost is "rename everywhere." A second -namespace (`.spaceExt.*` or `.contrail.*`) for clearly-off-spec -features would keep the `space.*` surface close to whatever the spec becomes. - -Decision: split them. Pick a namespace name, move at least `invite.*` and -`whoami`; `leaveSpace` is ambiguous. +## Namespace split for contrail-specific extras — done +`space.invite.*` and `space.whoami` moved to `.spaceExt.*`. See +[spaces-spec-mapping.md § Contrail extras](./spaces-spec-mapping.md#contrail-extras-namespace-nsspaceext). +`leaveSpace` is still in `space.*` — revisit if the spec lands with different +self-remove semantics. ## Ownership transfer Dropped for now. The space URI is `at:////` — owner DID diff --git a/docs/spaces-spec-mapping.md b/docs/spaces-spec-mapping.md index 36c9ccf..3368191 100644 --- a/docs/spaces-spec-mapping.md +++ b/docs/spaces-spec-mapping.md @@ -44,7 +44,6 @@ All endpoints are emitted under `.space.*` from templates in - `space.listMembers` — members for a space (member/owner only) - `space.listRecords` — space-scoped record listing; bearer-read supported - `space.getRecord` — single record; bearer-read supported -- `space.whoami` — caller's relationship to a space (extra; not in spec) ### Write - `space.putRecord` @@ -56,11 +55,16 @@ All endpoints are emitted under `.space.*` from templates in - `space.removeMember` - `space.leaveSpace` — self-remove; owner cannot leave (extra) -### Invites (extra; not in the spec) -- `space.invite.create` — returns raw token once; hash stored -- `space.invite.redeem` -- `space.invite.list` -- `space.invite.revoke` +### Contrail extras (namespace: `.spaceExt.*`) +Clearly-off-spec features live under a separate namespace so the `space.*` +surface stays close to whatever the permissioned-data spec becomes. Moved here +from `space.*` in an earlier refactor. + +- `spaceExt.whoami` — caller's relationship to a space (owner / member flags) +- `spaceExt.invite.create` — returns raw token once; hash stored +- `spaceExt.invite.redeem` +- `spaceExt.invite.list` +- `spaceExt.invite.revoke` Invites have three kinds: `join`, `read`, `read-join`. `read` tokens grant bearer-only anonymous read access; `read-join` does both; `join` requires a diff --git a/docs/todo.md b/docs/todo.md deleted file mode 100644 index 6e2293a..0000000 --- a/docs/todo.md +++ /dev/null @@ -1,4 +0,0 @@ -- permissioned blobs -- websockets/sse/webhooks -- add ingestion logic? -- public spaces? \ No newline at end of file diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json new file mode 100644 index 0000000..c128f52 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json @@ -0,0 +1,114 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.channel.watchRecords", + "defs": { + "main": { + "type": "query", + "description": "Subscribe to a live tools.atmo.chat.channel query. Returns Server-Sent Events: `snapshot.start`, `snapshot.record` (N of), `snapshot.end`, then `record.created` / `record.deleted` as records enter/leave the result, plus a final `member.removed` if the caller loses access mid-stream. v1 requires spaceUri; cross-space watch is deferred.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + }, + "communityDid": { + "type": "string", + "description": "Filter by communityDid" + }, + "name": { + "type": "string", + "description": "Filter by name" + }, + "topic": { + "type": "string", + "description": "Filter by topic" + }, + "visibility": { + "type": "string", + "description": "Filter by visibility" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "sort": { + "type": "string", + "knownValues": [ + "communityDid", + "name", + "topic", + "visibility", + "createdAt" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "text/event-stream", + "schema": { + "type": "object", + "properties": {} + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "AuthRequired" + }, + { + "name": "Forbidden" + }, + { + "name": "NotFound" + }, + { + "name": "NotSupported" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json similarity index 95% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json index 92b1f97..457244d 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/whoami.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.chat.community.whoami", + "id": "tools.atmo.chat.community.space.whoami", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json new file mode 100644 index 0000000..b5e607a --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json @@ -0,0 +1,104 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.message.watchRecords", + "defs": { + "main": { + "type": "query", + "description": "Subscribe to a live tools.atmo.chat.message query. Returns Server-Sent Events: `snapshot.start`, `snapshot.record` (N of), `snapshot.end`, then `record.created` / `record.deleted` as records enter/leave the result, plus a final `member.removed` if the caller loses access mid-stream. v1 requires spaceUri; cross-space watch is deferred.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + }, + "text": { + "type": "string", + "description": "Filter by text" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "replyTo": { + "type": "string", + "description": "Filter by replyTo" + }, + "sort": { + "type": "string", + "knownValues": [ + "text", + "createdAt", + "replyTo" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "text/event-stream", + "schema": { + "type": "object", + "properties": {} + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "AuthRequired" + }, + { + "name": "Forbidden" + }, + { + "name": "NotFound" + }, + { + "name": "NotSupported" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json index 1704d8a..99c71cf 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json @@ -14,6 +14,7 @@ "lxm": [ "tools.atmo.chat.channel.getRecord", "tools.atmo.chat.channel.listRecords", + "tools.atmo.chat.channel.watchRecords", "tools.atmo.chat.community.adopt", "tools.atmo.chat.community.delete", "tools.atmo.chat.community.deleteRecord", @@ -35,27 +36,25 @@ "tools.atmo.chat.community.space.resync", "tools.atmo.chat.community.space.revoke", "tools.atmo.chat.community.space.setAccessLevel", - "tools.atmo.chat.community.whoami", + "tools.atmo.chat.community.space.whoami", "tools.atmo.chat.getCursor", "tools.atmo.chat.getOverview", "tools.atmo.chat.getProfile", "tools.atmo.chat.message.getRecord", "tools.atmo.chat.message.listRecords", + "tools.atmo.chat.message.watchRecords", "tools.atmo.chat.notifyOfUpdate", "tools.atmo.chat.realtime.subscribe", "tools.atmo.chat.realtime.ticket", "tools.atmo.chat.server.getRecord", "tools.atmo.chat.server.listRecords", + "tools.atmo.chat.server.watchRecords", "tools.atmo.chat.space.addMember", "tools.atmo.chat.space.createSpace", "tools.atmo.chat.space.deleteRecord", "tools.atmo.chat.space.getBlob", "tools.atmo.chat.space.getRecord", "tools.atmo.chat.space.getSpace", - "tools.atmo.chat.space.invite.create", - "tools.atmo.chat.space.invite.list", - "tools.atmo.chat.space.invite.redeem", - "tools.atmo.chat.space.invite.revoke", "tools.atmo.chat.space.leaveSpace", "tools.atmo.chat.space.listBlobs", "tools.atmo.chat.space.listMembers", @@ -64,7 +63,11 @@ "tools.atmo.chat.space.putRecord", "tools.atmo.chat.space.removeMember", "tools.atmo.chat.space.uploadBlob", - "tools.atmo.chat.space.whoami" + "tools.atmo.chat.spaceExt.invite.create", + "tools.atmo.chat.spaceExt.invite.list", + "tools.atmo.chat.spaceExt.invite.redeem", + "tools.atmo.chat.spaceExt.invite.revoke", + "tools.atmo.chat.spaceExt.whoami" ] } ] diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json new file mode 100644 index 0000000..9a8bbe7 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json @@ -0,0 +1,109 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.server.watchRecords", + "defs": { + "main": { + "type": "query", + "description": "Subscribe to a live tools.atmo.chat.server query. Returns Server-Sent Events: `snapshot.start`, `snapshot.record` (N of), `snapshot.end`, then `record.created` / `record.deleted` as records enter/leave the result, plus a final `member.removed` if the caller loses access mid-stream. v1 requires spaceUri; cross-space watch is deferred.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT or a read-grant invite token)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, + "inviteToken": { + "type": "string", + "description": "Read-grant invite token for anonymous bearer access. Replaces JWT auth when supplied." + }, + "communityDid": { + "type": "string", + "description": "Filter by communityDid" + }, + "name": { + "type": "string", + "description": "Filter by name" + }, + "description": { + "type": "string", + "description": "Filter by description" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "sort": { + "type": "string", + "knownValues": [ + "communityDid", + "name", + "description", + "createdAt" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "text/event-stream", + "schema": { + "type": "object", + "properties": {} + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "AuthRequired" + }, + { + "name": "Forbidden" + }, + { + "name": "NotFound" + }, + { + "name": "NotSupported" + } + ] + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/create.json similarity index 97% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/create.json index dc058ac..f6ed9fe 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/create.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/create.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.chat.space.invite.create", + "id": "tools.atmo.chat.spaceExt.invite.create", "defs": { "main": { "type": "procedure", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/list.json similarity index 95% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/list.json index aa3924f..d5191da 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/list.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/list.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.chat.space.invite.list", + "id": "tools.atmo.chat.spaceExt.invite.list", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json similarity index 94% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json index 8398704..9d9e552 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/redeem.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.chat.space.invite.redeem", + "id": "tools.atmo.chat.spaceExt.invite.redeem", "defs": { "main": { "type": "procedure", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/revoke.json similarity index 95% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/revoke.json index 3ccbace..49d5d2e 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/invite/revoke.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/revoke.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.chat.space.invite.revoke", + "id": "tools.atmo.chat.spaceExt.invite.revoke", "defs": { "main": { "type": "procedure", diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json similarity index 95% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json index 8e4f95f..83d5849 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/whoami.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.chat.space.whoami", + "id": "tools.atmo.chat.spaceExt.whoami", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts b/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts index e5d4872..0cfd48f 100644 --- a/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts +++ b/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts @@ -4,6 +4,7 @@ export const xrpcMethods = [ "tools.atmo.chat.channel.getRecord", "tools.atmo.chat.channel.listRecords", + "tools.atmo.chat.channel.watchRecords", "tools.atmo.chat.community.adopt", "tools.atmo.chat.community.delete", "tools.atmo.chat.community.deleteRecord", @@ -25,27 +26,25 @@ export const xrpcMethods = [ "tools.atmo.chat.community.space.resync", "tools.atmo.chat.community.space.revoke", "tools.atmo.chat.community.space.setAccessLevel", - "tools.atmo.chat.community.whoami", + "tools.atmo.chat.community.space.whoami", "tools.atmo.chat.getCursor", "tools.atmo.chat.getOverview", "tools.atmo.chat.getProfile", "tools.atmo.chat.message.getRecord", "tools.atmo.chat.message.listRecords", + "tools.atmo.chat.message.watchRecords", "tools.atmo.chat.notifyOfUpdate", "tools.atmo.chat.realtime.subscribe", "tools.atmo.chat.realtime.ticket", "tools.atmo.chat.server.getRecord", "tools.atmo.chat.server.listRecords", + "tools.atmo.chat.server.watchRecords", "tools.atmo.chat.space.addMember", "tools.atmo.chat.space.createSpace", "tools.atmo.chat.space.deleteRecord", "tools.atmo.chat.space.getBlob", "tools.atmo.chat.space.getRecord", "tools.atmo.chat.space.getSpace", - "tools.atmo.chat.space.invite.create", - "tools.atmo.chat.space.invite.list", - "tools.atmo.chat.space.invite.redeem", - "tools.atmo.chat.space.invite.revoke", "tools.atmo.chat.space.leaveSpace", "tools.atmo.chat.space.listBlobs", "tools.atmo.chat.space.listMembers", @@ -54,5 +53,9 @@ export const xrpcMethods = [ "tools.atmo.chat.space.putRecord", "tools.atmo.chat.space.removeMember", "tools.atmo.chat.space.uploadBlob", - "tools.atmo.chat.space.whoami" + "tools.atmo.chat.spaceExt.invite.create", + "tools.atmo.chat.spaceExt.invite.list", + "tools.atmo.chat.spaceExt.invite.redeem", + "tools.atmo.chat.spaceExt.invite.revoke", + "tools.atmo.chat.spaceExt.whoami" ] as const; diff --git a/examples/sveltekit-group-chat/src/lib/contrail/index.ts b/examples/sveltekit-group-chat/src/lib/contrail/index.ts index 0c0b382..13f2473 100644 --- a/examples/sveltekit-group-chat/src/lib/contrail/index.ts +++ b/examples/sveltekit-group-chat/src/lib/contrail/index.ts @@ -52,23 +52,20 @@ function build(env: Env): Bundle { maxSize: 2 * 1024 * 1024, accept: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] }, - // Dev-only bypass: trust the HMAC-signed `did` cookie the OAuth flow - // sets, in lieu of a service-auth JWT. bsky.social rejects - // `getServiceAuth` for loopback clients, so without this the demo - // would need a tunnel. NEVER set DEV_AUTH=1 in prod. + // Dev-only bypass: trust the HMAC-signed session cookie (or an + // X-Dev-Did header for server-side synthetic requests) as auth, so + // the loopback OAuth client doesn't need to round-trip through the + // user's PDS for getServiceAuth (bsky.social rejects that on + // loopback). In prod, DEV_AUTH is unset and every caller — browser + // or third-party — must present a real atproto service-auth JWT. + // Browsers get one by calling our SvelteKit helper which uses the + // OAuth session to mint via the user's PDS. authOverride: devAuth ? (req: Request) => { - // Accept either the HMAC-signed session cookie (from the - // browser via /xrpc/...) or an X-Dev-Did header (from the - // server-side remote helpers, which construct synthetic - // requests and already trust the session themselves). const headerDid = req.headers.get('x-dev-did'); const did = headerDid ?? getSignedCookieFromRequest(req, 'did'); if (!did) return null; - return { - issuer: did, - audience: env.SERVICE_DID - }; + return { issuer: did, audience: env.SERVICE_DID }; } : undefined }, diff --git a/examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts new file mode 100644 index 0000000..831e4e8 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts @@ -0,0 +1,35 @@ +/** Cross-component connection status for the realtime subscription powering + * the current view. The channel page updates this as its WatchQuery status + * changes; the server navbar renders a tiny colored dot for it. */ + +import type { WatchStoreStatus } from '@atmo-dev/contrail/sync'; + +export const connection = $state<{ status: WatchStoreStatus }>({ status: 'idle' }); + +export function setConnectionStatus(status: WatchStoreStatus): void { + connection.status = status; +} + +export function resetConnectionStatus(): void { + connection.status = 'idle'; +} + +/** Map status → (color, label) for the UI dot. */ +export function connectionIndicator(status: WatchStoreStatus): { + color: 'green' | 'orange' | 'red' | 'gray'; + label: string; +} { + switch (status) { + case 'live': + return { color: 'green', label: 'Connected' }; + case 'connecting': + case 'snapshot': + case 'reconnecting': + return { color: 'orange', label: 'Connecting…' }; + case 'closed': + return { color: 'red', label: 'Disconnected' }; + case 'idle': + default: + return { color: 'gray', label: 'Idle' }; + } +} diff --git a/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts index 3b03d1f..70bd04a 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts @@ -110,37 +110,42 @@ export function connectCommunityRealtime(communityDid: string): () => void { } catch { return; } + // Payload shape mirrors listRecords output (uri/did/collection/rkey/cid/record/time_us/space?). + // `space` is only set for space records — which is what we filter this community stream to. if (kind === 'record.created') { const p = ev.payload as { - spaceUri: string; + uri: string; + did: string; collection: string; - authorDid: string; rkey: string; record: Record; - createdAt: number | string; + time_us: number; + space?: string; }; + if (!p.space) return; if (p.collection === 'tools.atmo.chat.message') { const rec = p.record as { text?: string; createdAt?: string; replyTo?: string }; if (rec.text && rec.createdAt) { - channelMessages.append(p.spaceUri, { + channelMessages.append(p.space, { rkey: p.rkey, - authorDid: p.authorDid, + authorDid: p.did, text: rec.text, createdAt: rec.createdAt, replyTo: rec.replyTo }); // Bump unread if we're not currently on this channel. - if (!isCurrentChannel(p.spaceUri)) { - bumpUnread(p.spaceUri, rec.createdAt); + if (!isCurrentChannel(p.space)) { + bumpUnread(p.space, rec.createdAt); } } } else if (p.collection === 'tools.atmo.chat.channel') { void invalidateAll(); } } else if (kind === 'record.deleted') { - const p = ev.payload as { spaceUri: string; collection: string; rkey: string }; + const p = ev.payload as { uri: string; did: string; collection: string; rkey: string; space?: string }; + if (!p.space) return; if (p.collection === 'tools.atmo.chat.message') { - channelMessages.remove(p.spaceUri, p.rkey); + channelMessages.remove(p.space, p.rkey); } else if (p.collection === 'tools.atmo.chat.channel') { void invalidateAll(); } diff --git a/examples/sveltekit-group-chat/src/lib/rooms/server.ts b/examples/sveltekit-group-chat/src/lib/rooms/server.ts index a8b429d..c8e0e99 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/server.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/server.ts @@ -213,7 +213,7 @@ export async function whoami( ctx: AuthedCallContext, query: { spaceUri: string } ): Promise<{ spaceUri: string; accessLevel: string | null }> { - return callContrail(ctx, 'tools.atmo.chat.community.whoami', { query }); + return callContrail(ctx, 'tools.atmo.chat.community.space.whoami', { query }); } // --- realtime -------------------------------------------------------------- diff --git a/examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts b/examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts new file mode 100644 index 0000000..a54999c --- /dev/null +++ b/examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts @@ -0,0 +1,25 @@ +import { error, json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +/** POST { lxm } → { token } — mint an atproto service-auth JWT scoped to one + * XRPC method, using the caller's OAuth session to call getServiceAuth on + * their PDS. Used by the browser sync engine to auth the watchRecords + * handshake fetch — it can't mint JWTs itself, but same-origin to us it + * can delegate. + * + * Cross-origin apps skip this and mint their own JWTs server-side. */ +export const POST: RequestHandler = async ({ request, locals, platform }) => { + if (!locals.did || !locals.client) error(401, 'Not authenticated'); + const body = (await request.json().catch(() => null)) as { lxm?: string } | null; + if (!body?.lxm) error(400, 'lxm required'); + + const res = await locals.client.get('com.atproto.server.getServiceAuth', { + params: { + aud: platform!.env.SERVICE_DID as `did:${string}:${string}`, + lxm: body.lxm as `${string}.${string}.${string}`, + exp: Math.floor(Date.now() / 1000) + 120 + } + }); + if (!res.ok) error(502, `getServiceAuth failed: ${JSON.stringify(res.data)}`); + return json({ token: (res.data as { token: string }).token }); +}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts index f444a2d..d3f554a 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts @@ -118,7 +118,7 @@ export const load: LayoutServerLoad = async ({ locals, params, platform }) => { const adminUri = buildAdminUri(communityDid); const d = await authedFetch<{ accessLevel: string | null }>( ctx, - 'tools.atmo.chat.community.whoami', + 'tools.atmo.chat.community.space.whoami', { query: { spaceUri: adminUri } } ); isAdmin = d.accessLevel === 'admin' || d.accessLevel === 'owner'; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte index 34c8e64..27980bc 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte @@ -7,6 +7,7 @@ import { createChannel } from '$lib/rooms/rooms.remote'; import { unread, resetUnread } from '$lib/rooms/unread.svelte'; import { connectCommunityRealtime } from '$lib/rooms/realtime.svelte'; + import { connection, connectionIndicator } from '$lib/rooms/connection.svelte'; let { data, children } = $props(); @@ -116,6 +117,24 @@ {data.server?.name ?? 'server'} {/if}

+ + {@const ind = connectionIndicator(connection.status)} + + + + diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte index fa27463..4814463 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte @@ -9,6 +9,7 @@ import { createWatchQuery } from '$lib/rooms/watch.svelte'; import type { WatchRecord } from '@atmo-dev/contrail/sync'; import { dev } from '$app/environment'; + import { setConnectionStatus, resetConnectionStatus } from '$lib/rooms/connection.svelte'; let { data } = $props(); @@ -18,22 +19,46 @@ // Live message feed via contrail's watchRecords subscription. The engine // handles the snapshot + live merge; we just render its `records` array. // Sorted newest-first by default — flip to oldest-first for chat. - let query = $derived.by(() => - createWatchQuery({ - url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(data.spaceUri)}&limit=50`, - // In prod the server has a DurableObjectPubSub — open a DO-terminated - // WS so idle connections hibernate (near-zero cost at CF scale). - // In dev the server uses InMemoryPubSub (one isolate), which only - // speaks SSE, so downgrade. + // + // The query is keyed strictly on spaceUri — recreating it on every + // `data` identity change would tear down and rebuild the subscription on + // unrelated page-data invalidations (e.g. channel list refresh), flashing + // "Loading…" each time. Using a keyed effect here avoids that. + let query = $state | null>(null); + let currentUri: string | null = null; + + $effect(() => { + const uri = data.spaceUri; + if (uri === currentUri) return; + query?.stop(); + currentUri = uri; + query = createWatchQuery({ + url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(uri)}&limit=50`, transport: dev ? 'sse' : 'ws', + fetchAuthToken: dev + ? undefined + : async () => { + const res = await fetch('/api/watch-token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ lxm: 'tools.atmo.chat.message.watchRecords' }) + }); + if (!res.ok) throw new Error(`watch-token mint failed: ${res.status}`); + const d = (await res.json()) as { token: string }; + return d.token; + }, compareRecords: (a: WatchRecord, b: WatchRecord) => (a.time_us ?? 0) - (b.time_us ?? 0) - }) - ); + }); + }); - // Teardown on channel change. + // Final teardown on component unmount. $effect(() => { - return () => query.stop(); + return () => { + query?.stop(); + query = null; + currentUri = null; + }; }); // Unread / current-channel bookkeeping. @@ -43,9 +68,16 @@ return () => setCurrentChannel(null); }); + // Mirror the query's connection status into the shared store so the + // layout's navbar can render a dot for it. + $effect(() => { + if (query) setConnectionStatus(query.status); + return () => resetConnectionStatus(); + }); + // Project records to the shape the existing template expects. let messages = $derived( - query.records.map((r) => { + (query?.records ?? []).map((r) => { const rec = r.record as { text?: string; createdAt?: string; replyTo?: string }; return { rkey: r.rkey, @@ -94,7 +126,7 @@
- {#if query.status === 'connecting' || query.status === 'snapshot'} + {#if messages.length === 0 && (query?.status === 'connecting' || query?.status === 'snapshot' || query?.status === 'idle')}
Loading…
{:else if messages.length === 0}
No messages yet. Say hi.
diff --git a/lexicons-generated/rsvp/atmo/community/defs.json b/lexicons-generated/rsvp/atmo/community/defs.json index 1471660..6f64581 100644 --- a/lexicons-generated/rsvp/atmo/community/defs.json +++ b/lexicons-generated/rsvp/atmo/community/defs.json @@ -130,6 +130,56 @@ "type": "integer" } } + }, + "inviteView": { + "type": "object", + "required": [ + "tokenHash", + "spaceUri", + "accessLevel", + "createdBy", + "createdAt", + "usedCount" + ], + "properties": { + "tokenHash": { + "type": "string", + "description": "SHA-256 of the raw token. Stable id for list/revoke; never grants access on its own." + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "#accessLevel" + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms. Omitted for no expiry." + }, + "maxUses": { + "type": "integer", + "description": "Omitted for unlimited." + }, + "usedCount": { + "type": "integer" + }, + "revokedAt": { + "type": "integer", + "description": "Unix ms. Omitted if not revoked." + }, + "note": { + "type": "string" + } + } } } } diff --git a/lexicons-generated/rsvp/atmo/community/invite/create.json b/lexicons-generated/rsvp/atmo/community/invite/create.json new file mode 100644 index 0000000..9b4d211 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/invite/create.json @@ -0,0 +1,93 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.invite.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "accessLevel" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#accessLevel" + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms. Omit for no expiry." + }, + "maxUses": { + "type": "integer", + "minimum": 1, + "description": "Omit for unlimited redemptions." + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token", + "tokenHash", + "spaceUri", + "accessLevel", + "createdAt" + ], + "properties": { + "token": { + "type": "string", + "description": "Raw token. Shown once — cannot be retrieved later." + }, + "tokenHash": { + "type": "string" + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#accessLevel" + }, + "expiresAt": { + "type": "integer" + }, + "maxUses": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "Forbidden" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/invite/list.json b/lexicons-generated/rsvp/atmo/community/invite/list.json new file mode 100644 index 0000000..7bd0010 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/invite/list.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.invite.list", + "defs": { + "main": { + "type": "query", + "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "includeRevoked": { + "type": "boolean", + "default": false + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "invites" + ], + "properties": { + "invites": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#inviteView" + } + } + } + } + }, + "errors": [ + { + "name": "Forbidden" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/invite/redeem.json b/lexicons-generated/rsvp/atmo/community/invite/redeem.json new file mode 100644 index 0000000..0995e9b --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/invite/redeem.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.invite.redeem", + "defs": { + "main": { + "type": "procedure", + "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "accessLevel", + "communityDid" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "accessLevel": { + "type": "ref", + "ref": "rsvp.atmo.community.defs#accessLevel" + }, + "communityDid": { + "type": "string", + "format": "did" + } + } + } + }, + "errors": [ + { + "name": "InvalidRequest" + }, + { + "name": "InvalidToken" + }, + { + "name": "NotFound" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/invite/revoke.json b/lexicons-generated/rsvp/atmo/community/invite/revoke.json new file mode 100644 index 0000000..c7aa335 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/community/invite/revoke.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.community.invite.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "tokenHash" + ], + "properties": { + "tokenHash": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/community/whoami.json b/lexicons-generated/rsvp/atmo/community/space/whoami.json similarity index 95% rename from lexicons-generated/rsvp/atmo/community/whoami.json rename to lexicons-generated/rsvp/atmo/community/space/whoami.json index 691d361..524345d 100644 --- a/lexicons-generated/rsvp/atmo/community/whoami.json +++ b/lexicons-generated/rsvp/atmo/community/space/whoami.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "rsvp.atmo.community.whoami", + "id": "rsvp.atmo.community.space.whoami", "defs": { "main": { "type": "query", diff --git a/lexicons-generated/rsvp/atmo/event/getRecord.json b/lexicons-generated/rsvp/atmo/event/getRecord.json index d2dc8a6..ab6b24d 100644 --- a/lexicons-generated/rsvp/atmo/event/getRecord.json +++ b/lexicons-generated/rsvp/atmo/event/getRecord.json @@ -213,11 +213,8 @@ "type": "string" }, "record": { - "type": "union", - "refs": [ - "#appBskyActorProfile", - "#siteStandardPublication" - ] + "type": "ref", + "ref": "#appBskyActorProfile" } } }, @@ -283,57 +280,6 @@ "type": "ref" } } - }, - "siteStandardPublication": { - "type": "object", - "required": [ - "url", - "name" - ], - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." - }, - "icon": { - "type": "blob", - "accept": [ - "image/*" - ], - "maxSize": 1000000, - "description": "Square image to identify the publication. Should be at least 256x256." - }, - "name": { - "type": "string", - "maxLength": 5000, - "description": "Name of the publication.", - "maxGraphemes": 500 - }, - "labels": { - "refs": [ - "com.atproto.label.defs#selfLabels" - ], - "type": "union", - "description": "Self-label values for this publication. Effectively content warnings." - }, - "basicTheme": { - "ref": "site.standard.theme.basic", - "type": "ref", - "description": "Simplified publication theme for tools and apps to utilize when displaying content." - }, - "description": { - "type": "string", - "maxLength": 30000, - "description": "Brief description of the publication.", - "maxGraphemes": 3000 - }, - "preferences": { - "ref": "#preferences", - "type": "ref", - "description": "Object containing platform specific preferences (with a few shared properties)." - } - } } } } diff --git a/lexicons-generated/rsvp/atmo/event/listRecords.json b/lexicons-generated/rsvp/atmo/event/listRecords.json index 3f938e0..1c68aee 100644 --- a/lexicons-generated/rsvp/atmo/event/listRecords.json +++ b/lexicons-generated/rsvp/atmo/event/listRecords.json @@ -327,11 +327,8 @@ "type": "string" }, "record": { - "type": "union", - "refs": [ - "#appBskyActorProfile", - "#siteStandardPublication" - ] + "type": "ref", + "ref": "#appBskyActorProfile" } } }, @@ -397,57 +394,6 @@ "type": "ref" } } - }, - "siteStandardPublication": { - "type": "object", - "required": [ - "url", - "name" - ], - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." - }, - "icon": { - "type": "blob", - "accept": [ - "image/*" - ], - "maxSize": 1000000, - "description": "Square image to identify the publication. Should be at least 256x256." - }, - "name": { - "type": "string", - "maxLength": 5000, - "description": "Name of the publication.", - "maxGraphemes": 500 - }, - "labels": { - "refs": [ - "com.atproto.label.defs#selfLabels" - ], - "type": "union", - "description": "Self-label values for this publication. Effectively content warnings." - }, - "basicTheme": { - "ref": "site.standard.theme.basic", - "type": "ref", - "description": "Simplified publication theme for tools and apps to utilize when displaying content." - }, - "description": { - "type": "string", - "maxLength": 30000, - "description": "Brief description of the publication.", - "maxGraphemes": 3000 - }, - "preferences": { - "ref": "#preferences", - "type": "ref", - "description": "Object containing platform specific preferences (with a few shared properties)." - } - } } } } diff --git a/lexicons-generated/rsvp/atmo/getProfile.json b/lexicons-generated/rsvp/atmo/getProfile.json index d6c6191..d6c1fe5 100644 --- a/lexicons-generated/rsvp/atmo/getProfile.json +++ b/lexicons-generated/rsvp/atmo/getProfile.json @@ -65,11 +65,8 @@ "type": "string" }, "record": { - "type": "union", - "refs": [ - "#appBskyActorProfile", - "#siteStandardPublication" - ] + "type": "ref", + "ref": "#appBskyActorProfile" } } }, @@ -135,57 +132,6 @@ "type": "ref" } } - }, - "siteStandardPublication": { - "type": "object", - "required": [ - "url", - "name" - ], - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." - }, - "icon": { - "type": "blob", - "accept": [ - "image/*" - ], - "maxSize": 1000000, - "description": "Square image to identify the publication. Should be at least 256x256." - }, - "name": { - "type": "string", - "maxLength": 5000, - "description": "Name of the publication.", - "maxGraphemes": 500 - }, - "labels": { - "refs": [ - "com.atproto.label.defs#selfLabels" - ], - "type": "union", - "description": "Self-label values for this publication. Effectively content warnings." - }, - "basicTheme": { - "ref": "site.standard.theme.basic", - "type": "ref", - "description": "Simplified publication theme for tools and apps to utilize when displaying content." - }, - "description": { - "type": "string", - "maxLength": 30000, - "description": "Brief description of the publication.", - "maxGraphemes": 3000 - }, - "preferences": { - "ref": "#preferences", - "type": "ref", - "description": "Object containing platform specific preferences (with a few shared properties)." - } - } } } } diff --git a/lexicons-generated/rsvp/atmo/permissionSet.json b/lexicons-generated/rsvp/atmo/permissionSet.json index 9ac2975..5fae345 100644 --- a/lexicons-generated/rsvp/atmo/permissionSet.json +++ b/lexicons-generated/rsvp/atmo/permissionSet.json @@ -16,6 +16,10 @@ "rsvp.atmo.community.delete", "rsvp.atmo.community.deleteRecord", "rsvp.atmo.community.getHealth", + "rsvp.atmo.community.invite.create", + "rsvp.atmo.community.invite.list", + "rsvp.atmo.community.invite.redeem", + "rsvp.atmo.community.invite.revoke", "rsvp.atmo.community.list", "rsvp.atmo.community.mint", "rsvp.atmo.community.putRecord", @@ -29,7 +33,7 @@ "rsvp.atmo.community.space.resync", "rsvp.atmo.community.space.revoke", "rsvp.atmo.community.space.setAccessLevel", - "rsvp.atmo.community.whoami", + "rsvp.atmo.community.space.whoami", "rsvp.atmo.event.getRecord", "rsvp.atmo.event.listRecords", "rsvp.atmo.getCursor", @@ -41,19 +45,22 @@ "rsvp.atmo.space.addMember", "rsvp.atmo.space.createSpace", "rsvp.atmo.space.deleteRecord", + "rsvp.atmo.space.getBlob", "rsvp.atmo.space.getRecord", "rsvp.atmo.space.getSpace", - "rsvp.atmo.space.invite.create", - "rsvp.atmo.space.invite.list", - "rsvp.atmo.space.invite.redeem", - "rsvp.atmo.space.invite.revoke", "rsvp.atmo.space.leaveSpace", + "rsvp.atmo.space.listBlobs", "rsvp.atmo.space.listMembers", "rsvp.atmo.space.listRecords", "rsvp.atmo.space.listSpaces", "rsvp.atmo.space.putRecord", "rsvp.atmo.space.removeMember", - "rsvp.atmo.space.whoami" + "rsvp.atmo.space.uploadBlob", + "rsvp.atmo.spaceExt.invite.create", + "rsvp.atmo.spaceExt.invite.list", + "rsvp.atmo.spaceExt.invite.redeem", + "rsvp.atmo.spaceExt.invite.revoke", + "rsvp.atmo.spaceExt.whoami" ] } ] diff --git a/lexicons-generated/rsvp/atmo/rsvp/getRecord.json b/lexicons-generated/rsvp/atmo/rsvp/getRecord.json index d63df47..6bb1bcf 100644 --- a/lexicons-generated/rsvp/atmo/rsvp/getRecord.json +++ b/lexicons-generated/rsvp/atmo/rsvp/getRecord.json @@ -162,11 +162,8 @@ "type": "string" }, "record": { - "type": "union", - "refs": [ - "#appBskyActorProfile", - "#siteStandardPublication" - ] + "type": "ref", + "ref": "#appBskyActorProfile" } } }, @@ -232,57 +229,6 @@ "type": "ref" } } - }, - "siteStandardPublication": { - "type": "object", - "required": [ - "url", - "name" - ], - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." - }, - "icon": { - "type": "blob", - "accept": [ - "image/*" - ], - "maxSize": 1000000, - "description": "Square image to identify the publication. Should be at least 256x256." - }, - "name": { - "type": "string", - "maxLength": 5000, - "description": "Name of the publication.", - "maxGraphemes": 500 - }, - "labels": { - "refs": [ - "com.atproto.label.defs#selfLabels" - ], - "type": "union", - "description": "Self-label values for this publication. Effectively content warnings." - }, - "basicTheme": { - "ref": "site.standard.theme.basic", - "type": "ref", - "description": "Simplified publication theme for tools and apps to utilize when displaying content." - }, - "description": { - "type": "string", - "maxLength": 30000, - "description": "Brief description of the publication.", - "maxGraphemes": 3000 - }, - "preferences": { - "ref": "#preferences", - "type": "ref", - "description": "Object containing platform specific preferences (with a few shared properties)." - } - } } } } diff --git a/lexicons-generated/rsvp/atmo/rsvp/listRecords.json b/lexicons-generated/rsvp/atmo/rsvp/listRecords.json index d216597..3491b78 100644 --- a/lexicons-generated/rsvp/atmo/rsvp/listRecords.json +++ b/lexicons-generated/rsvp/atmo/rsvp/listRecords.json @@ -215,11 +215,8 @@ "type": "string" }, "record": { - "type": "union", - "refs": [ - "#appBskyActorProfile", - "#siteStandardPublication" - ] + "type": "ref", + "ref": "#appBskyActorProfile" } } }, @@ -285,57 +282,6 @@ "type": "ref" } } - }, - "siteStandardPublication": { - "type": "object", - "required": [ - "url", - "name" - ], - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." - }, - "icon": { - "type": "blob", - "accept": [ - "image/*" - ], - "maxSize": 1000000, - "description": "Square image to identify the publication. Should be at least 256x256." - }, - "name": { - "type": "string", - "maxLength": 5000, - "description": "Name of the publication.", - "maxGraphemes": 500 - }, - "labels": { - "refs": [ - "com.atproto.label.defs#selfLabels" - ], - "type": "union", - "description": "Self-label values for this publication. Effectively content warnings." - }, - "basicTheme": { - "ref": "site.standard.theme.basic", - "type": "ref", - "description": "Simplified publication theme for tools and apps to utilize when displaying content." - }, - "description": { - "type": "string", - "maxLength": 30000, - "description": "Brief description of the publication.", - "maxGraphemes": 3000 - }, - "preferences": { - "ref": "#preferences", - "type": "ref", - "description": "Object containing platform specific preferences (with a few shared properties)." - } - } } } } diff --git a/lexicons-generated/rsvp/atmo/space/invite/create.json b/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json similarity index 97% rename from lexicons-generated/rsvp/atmo/space/invite/create.json rename to lexicons-generated/rsvp/atmo/spaceExt/invite/create.json index c60f793..b604074 100644 --- a/lexicons-generated/rsvp/atmo/space/invite/create.json +++ b/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "rsvp.atmo.space.invite.create", + "id": "rsvp.atmo.spaceExt.invite.create", "defs": { "main": { "type": "procedure", diff --git a/lexicons-generated/rsvp/atmo/space/invite/list.json b/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json similarity index 96% rename from lexicons-generated/rsvp/atmo/space/invite/list.json rename to lexicons-generated/rsvp/atmo/spaceExt/invite/list.json index 848d4e2..304be52 100644 --- a/lexicons-generated/rsvp/atmo/space/invite/list.json +++ b/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "rsvp.atmo.space.invite.list", + "id": "rsvp.atmo.spaceExt.invite.list", "defs": { "main": { "type": "query", diff --git a/lexicons-generated/rsvp/atmo/space/invite/redeem.json b/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json similarity index 95% rename from lexicons-generated/rsvp/atmo/space/invite/redeem.json rename to lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json index 06261e9..b0fa07e 100644 --- a/lexicons-generated/rsvp/atmo/space/invite/redeem.json +++ b/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "rsvp.atmo.space.invite.redeem", + "id": "rsvp.atmo.spaceExt.invite.redeem", "defs": { "main": { "type": "procedure", diff --git a/lexicons-generated/rsvp/atmo/space/invite/revoke.json b/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json similarity index 95% rename from lexicons-generated/rsvp/atmo/space/invite/revoke.json rename to lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json index afa34f2..40c4293 100644 --- a/lexicons-generated/rsvp/atmo/space/invite/revoke.json +++ b/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "rsvp.atmo.space.invite.revoke", + "id": "rsvp.atmo.spaceExt.invite.revoke", "defs": { "main": { "type": "procedure", diff --git a/lexicons-generated/rsvp/atmo/space/whoami.json b/lexicons-generated/rsvp/atmo/spaceExt/whoami.json similarity index 96% rename from lexicons-generated/rsvp/atmo/space/whoami.json rename to lexicons-generated/rsvp/atmo/spaceExt/whoami.json index 8eb98a2..0f5d078 100644 --- a/lexicons-generated/rsvp/atmo/space/whoami.json +++ b/lexicons-generated/rsvp/atmo/spaceExt/whoami.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "rsvp.atmo.space.whoami", + "id": "rsvp.atmo.spaceExt.whoami", "defs": { "main": { "type": "query", diff --git a/lexicons-pulled/README.md b/lexicons-pulled/README.md index 69dd796..378ad0f 100644 --- a/lexicons-pulled/README.md +++ b/lexicons-pulled/README.md @@ -2,4 +2,4 @@ this directory contains lexicon documents pulled from the following sources: -- atproto (nsids: app.bsky.actor.profile, community.lexicon.calendar.event, community.lexicon.calendar.rsvp, community.lexicon.location.address, community.lexicon.location.fsq, community.lexicon.location.geo, community.lexicon.location.hthree, site.standard.publication, site.standard.theme.basic, site.standard.theme.color) +- atproto (nsids: app.bsky.actor.profile, community.lexicon.calendar.event, community.lexicon.calendar.rsvp, community.lexicon.location.address, community.lexicon.location.fsq, community.lexicon.location.geo, community.lexicon.location.hthree) diff --git a/lexicons-pulled/site/standard/publication.json b/lexicons-pulled/site/standard/publication.json deleted file mode 100644 index 9656716..0000000 --- a/lexicons-pulled/site/standard/publication.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "id": "site.standard.publication", - "defs": { - "main": { - "key": "tid", - "type": "record", - "record": { - "type": "object", - "required": ["url", "name"], - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." - }, - "icon": { - "type": "blob", - "accept": ["image/*"], - "maxSize": 1000000, - "description": "Square image to identify the publication. Should be at least 256x256." - }, - "name": { - "type": "string", - "maxLength": 5000, - "description": "Name of the publication.", - "maxGraphemes": 500 - }, - "labels": { - "refs": ["com.atproto.label.defs#selfLabels"], - "type": "union", - "description": "Self-label values for this publication. Effectively content warnings." - }, - "basicTheme": { - "ref": "site.standard.theme.basic", - "type": "ref", - "description": "Simplified publication theme for tools and apps to utilize when displaying content." - }, - "description": { - "type": "string", - "maxLength": 30000, - "description": "Brief description of the publication.", - "maxGraphemes": 3000 - }, - "preferences": { - "ref": "#preferences", - "type": "ref", - "description": "Object containing platform specific preferences (with a few shared properties)." - } - } - }, - "description": "A publication record representing a blog, website, or content platform. Publications serve as containers for documents and define the overall branding and settings." - }, - "preferences": { - "type": "object", - "properties": { - "showInDiscover": { - "type": "boolean", - "default": true, - "description": "Boolean which decides whether the publication should appear in discovery feeds." - } - } - } - }, - "$type": "com.atproto.lexicon.schema", - "lexicon": 1 -} diff --git a/lexicons-pulled/site/standard/theme/basic.json b/lexicons-pulled/site/standard/theme/basic.json deleted file mode 100644 index 88ff587..0000000 --- a/lexicons-pulled/site/standard/theme/basic.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "id": "site.standard.theme.basic", - "defs": { - "main": { - "key": "tid", - "type": "record", - "record": { - "type": "object", - "required": ["background", "foreground", "accent", "accentForeground"], - "properties": { - "accent": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for links and button backgrounds." - }, - "background": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for content background." - }, - "foreground": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for content text." - }, - "accentForeground": { - "refs": ["site.standard.theme.color#rgb"], - "type": "union", - "description": "Color used for button text." - } - } - }, - "description": "A simplified theme definition for publications, providing basic color customization for content display across different platforms and applications." - } - }, - "$type": "com.atproto.lexicon.schema", - "lexicon": 1 -} diff --git a/lexicons-pulled/site/standard/theme/color.json b/lexicons-pulled/site/standard/theme/color.json deleted file mode 100644 index 5dde553..0000000 --- a/lexicons-pulled/site/standard/theme/color.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "id": "site.standard.theme.color", - "defs": { - "rgb": { - "type": "object", - "required": ["r", "g", "b"], - "properties": { - "b": { - "type": "integer", - "maximum": 255, - "minimum": 0 - }, - "g": { - "type": "integer", - "maximum": 255, - "minimum": 0 - }, - "r": { - "type": "integer", - "maximum": 255, - "minimum": 0 - } - } - }, - "rgba": { - "type": "object", - "required": ["r", "g", "b", "a"], - "properties": { - "a": { - "type": "integer", - "maximum": 100, - "minimum": 0 - }, - "b": { - "type": "integer", - "maximum": 255, - "minimum": 0 - }, - "g": { - "type": "integer", - "maximum": 255, - "minimum": 0 - }, - "r": { - "type": "integer", - "maximum": 255, - "minimum": 0 - } - } - } - }, - "$type": "com.atproto.lexicon.schema", - "lexicon": 1 -} diff --git a/spaces-lexicon-templates/invite/create.json b/spaces-lexicon-templates/ext/invite/create.json similarity index 97% rename from spaces-lexicon-templates/invite/create.json rename to spaces-lexicon-templates/ext/invite/create.json index e3cdf75..4450aed 100644 --- a/spaces-lexicon-templates/invite/create.json +++ b/spaces-lexicon-templates/ext/invite/create.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.space.invite.create", + "id": "tools.atmo.spaceExt.invite.create", "defs": { "main": { "type": "procedure", diff --git a/spaces-lexicon-templates/invite/list.json b/spaces-lexicon-templates/ext/invite/list.json similarity index 95% rename from spaces-lexicon-templates/invite/list.json rename to spaces-lexicon-templates/ext/invite/list.json index 217e822..ed316f9 100644 --- a/spaces-lexicon-templates/invite/list.json +++ b/spaces-lexicon-templates/ext/invite/list.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.space.invite.list", + "id": "tools.atmo.spaceExt.invite.list", "defs": { "main": { "type": "query", diff --git a/spaces-lexicon-templates/invite/redeem.json b/spaces-lexicon-templates/ext/invite/redeem.json similarity index 94% rename from spaces-lexicon-templates/invite/redeem.json rename to spaces-lexicon-templates/ext/invite/redeem.json index 398127d..d0372c2 100644 --- a/spaces-lexicon-templates/invite/redeem.json +++ b/spaces-lexicon-templates/ext/invite/redeem.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.space.invite.redeem", + "id": "tools.atmo.spaceExt.invite.redeem", "defs": { "main": { "type": "procedure", diff --git a/spaces-lexicon-templates/invite/revoke.json b/spaces-lexicon-templates/ext/invite/revoke.json similarity index 94% rename from spaces-lexicon-templates/invite/revoke.json rename to spaces-lexicon-templates/ext/invite/revoke.json index e9d068b..13ecf62 100644 --- a/spaces-lexicon-templates/invite/revoke.json +++ b/spaces-lexicon-templates/ext/invite/revoke.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.space.invite.revoke", + "id": "tools.atmo.spaceExt.invite.revoke", "defs": { "main": { "type": "procedure", diff --git a/spaces-lexicon-templates/whoami.json b/spaces-lexicon-templates/ext/whoami.json similarity index 95% rename from spaces-lexicon-templates/whoami.json rename to spaces-lexicon-templates/ext/whoami.json index c0e8f9d..5736c86 100644 --- a/spaces-lexicon-templates/whoami.json +++ b/spaces-lexicon-templates/ext/whoami.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "tools.atmo.space.whoami", + "id": "tools.atmo.spaceExt.whoami", "defs": { "main": { "type": "query", diff --git a/src/contrail.ts b/src/contrail.ts index 268975e..38f04c1 100644 --- a/src/contrail.ts +++ b/src/contrail.ts @@ -11,6 +11,10 @@ import { processNotifyUris } from "./core/router/notify"; import type { NotifyResult } from "./core/router/notify"; import { runPersistent as runPersistentIngestion } from "./core/persistent"; import type { PersistentIngestOptions } from "./core/persistent"; +import type { PubSub } from "./core/realtime/types"; +import { InMemoryPubSub } from "./core/realtime/in-memory"; +import { createApp, type CreateAppOptions } from "./core/router"; +import type { Hono } from "hono"; export interface ContrailOptions extends ContrailConfig { db?: Database; @@ -23,6 +27,7 @@ export class Contrail { private _db?: Database; private _spacesDb?: Database; private _ingestState: IngestState = createIngestState(); + private _pubsub: PubSub | null = null; constructor(options: ContrailOptions) { const { db, spacesDb, ...configInput } = options; @@ -30,6 +35,18 @@ export class Contrail { validateConfig(this.config); this._db = db; this._spacesDb = spacesDb; + // Build the pubsub instance up-front so ingestion and HTTP routes share + // it. Caller overrides via `config.realtime.pubsub` (e.g. DurableObject). + if (this.config.realtime) { + this._pubsub = + this.config.realtime.pubsub ?? + new InMemoryPubSub({ queueBound: this.config.realtime.queueBound }); + } + } + + /** The shared realtime pubsub, or null when realtime isn't configured. */ + get pubsub(): PubSub | null { + return this._pubsub; } private getDb(db?: Database): Database { @@ -62,12 +79,22 @@ export class Contrail { /** Run one Jetstream ingestion cycle (catches up to present, then stops). */ async ingest(options?: { timeoutMs?: number }, db?: Database): Promise { - await runIngestCycle(this.getDb(db), this.config, options?.timeoutMs, this._ingestState); + await runIngestCycle( + this.getDb(db), + this.config, + options?.timeoutMs, + this._ingestState, + this._pubsub ?? undefined + ); } /** Run persistent Jetstream ingestion (long-lived, stays connected). */ async runPersistent(options?: Omit, db?: Database): Promise { - await runPersistentIngestion(this.getDb(db), this.config, { ...options, logger: this.config.logger }); + await runPersistentIngestion(this.getDb(db), this.config, { + ...options, + logger: this.config.logger, + pubsub: this._pubsub ?? undefined, + }); } /** Discover users from relays. Returns discovered DIDs. */ @@ -109,4 +136,33 @@ export class Contrail { const uriList = Array.isArray(uris) ? uris : [uris]; return processNotifyUris(this.getDb(db), this.config, uriList); } + + /** Build the Hono app for this Contrail instance. All HTTP routes + * (collection / spaces / community / realtime) are registered here. + * The realtime pubsub on this instance is reused, so subscribers see + * events published from `ingest()` / `runPersistent()` on the same instance. */ + app(options: AppOptions = {}): Hono { + const { db, ...appOpts } = options; + const main = this.getDb(db); + const spaces = options.spacesDb ?? this._spacesDb; + return createApp(main, this.config, { + ...appOpts, + spacesDb: spaces, + realtime: { ...appOpts.realtime, pubsub: this._pubsub ?? undefined }, + }); + } + + /** Fetch-style handler built from `app()`. Use this from SvelteKit / Next / + * Workers / Bun — anything that takes `(request) => Response`. */ + handler(options: AppOptions = {}): (request: Request) => Promise { + const app = this.app(options); + return (request: Request) => app.fetch(request) as Promise; + } +} + +/** Overrides accepted by `Contrail.app()` and `Contrail.handler()`. Mirrors + * `CreateAppOptions` but lets the caller also override the DBs (falling back + * to the ones given to the Contrail constructor). */ +export interface AppOptions extends CreateAppOptions { + db?: Database; } diff --git a/src/core/community/router.ts b/src/core/community/router.ts index 0318d7e..b3dfcd2 100644 --- a/src/core/community/router.ts +++ b/src/core/community/router.ts @@ -25,10 +25,7 @@ import { RESERVED_KEYS, } from "./types"; import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; -import { - generateInviteToken, - hashInviteToken, -} from "../spaces/invite-token"; +import { hashInviteToken, mintInviteToken } from "../invite/token"; export interface CommunityRoutesOptions { /** Override auth middleware for tests. */ @@ -257,7 +254,7 @@ export function registerCommunityRoutes( }); }); - app.get(`/xrpc/${NS}.whoami`, auth, async (c) => { + app.get(`/xrpc/${NS}.space.whoami`, auth, async (c) => { const sa = getAuth(c); const spaceUri = c.req.query("spaceUri"); if (!spaceUri) { @@ -1018,8 +1015,7 @@ export function registerCommunityRoutes( return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); } - const token = generateInviteToken(); - const tokenHash = await hashInviteToken(token); + const { token, tokenHash } = await mintInviteToken(); const row = await community.createInvite({ spaceUri: body.spaceUri, diff --git a/src/core/db/records.ts b/src/core/db/records.ts index c15f53d..b7ea175 100644 --- a/src/core/db/records.ts +++ b/src/core/db/records.ts @@ -391,6 +391,11 @@ export async function applyEvents( skipFeedFanout?: boolean; /** Pre-fetched existing records — skips the internal lookup when provided */ existing?: Map; + /** When provided, publish `collection:` and `actor:` realtime + * events for each applied event. Space-scoped publishing happens elsewhere + * (see `realtime/publishing-adapter.ts`); public topics carry public + * records only, which is exactly the scope of this function. */ + pubsub?: import("../realtime/types").PubSub; } ): Promise { if (events.length === 0) return; @@ -479,6 +484,47 @@ export async function applyEvents( } await db.batch(batch); + + // Publish realtime events for public records (collection: and actor:). + // Space records publish via the wrapping adapter; this path is public-only. + if (options?.pubsub) { + const pubsub = options.pubsub; + const ts = Date.now(); + for (const e of events) { + if (e.operation === "delete") { + const payload = { + uri: e.uri, + did: e.did, + collection: e.collection, + rkey: e.rkey, + }; + await pubsub.publish({ topic: `collection:${e.collection}`, kind: "record.deleted", payload, ts }); + await pubsub.publish({ topic: `actor:${e.did}`, kind: "record.deleted", payload, ts }); + } else { + const record = e.record ? safeParseJson(e.record) : {}; + const payload = { + uri: e.uri, + did: e.did, + collection: e.collection, + rkey: e.rkey, + cid: e.cid, + record, + time_us: e.time_us, + }; + await pubsub.publish({ topic: `collection:${e.collection}`, kind: "record.created", payload, ts }); + await pubsub.publish({ topic: `actor:${e.did}`, kind: "record.created", payload, ts }); + } + } + } +} + +function safeParseJson(s: string): Record { + try { + const v = JSON.parse(s); + return v && typeof v === "object" && !Array.isArray(v) ? (v as Record) : {}; + } catch { + return {}; + } } // --- Count columns --- diff --git a/src/core/invite/index.ts b/src/core/invite/index.ts new file mode 100644 index 0000000..d79fc3b --- /dev/null +++ b/src/core/invite/index.ts @@ -0,0 +1 @@ +export { generateInviteToken, hashInviteToken, mintInviteToken } from "./token"; diff --git a/src/core/spaces/invite-token.ts b/src/core/invite/token.ts similarity index 78% rename from src/core/spaces/invite-token.ts rename to src/core/invite/token.ts index 1fc1779..76f4880 100644 --- a/src/core/spaces/invite-token.ts +++ b/src/core/invite/token.ts @@ -33,3 +33,11 @@ export async function hashInviteToken(token: string): Promise { const digest = await crypto.subtle.digest("SHA-256", encoded); return bytesToHex(new Uint8Array(digest)); } + +/** Convenience: generate a token and return both the raw form (returned to + * the creator once) and its hash (persisted as the stable ID). */ +export async function mintInviteToken(): Promise<{ token: string; tokenHash: string }> { + const token = generateInviteToken(); + const tokenHash = await hashInviteToken(token); + return { token, tokenHash }; +} diff --git a/src/core/jetstream.ts b/src/core/jetstream.ts index fa74282..d42a5e8 100644 --- a/src/core/jetstream.ts +++ b/src/core/jetstream.ts @@ -180,7 +180,8 @@ export async function runIngestCycle( db: Database, config: ContrailConfig, timeoutMs: number = 25_000, - state?: IngestState + state?: IngestState, + pubsub?: import("./realtime/types").PubSub ): Promise { const log = getLogger(config); const s = state ?? createIngestState(); @@ -241,7 +242,7 @@ export async function runIngestCycle( for (let i = 0; i < events.length; i += BATCH_SIZE) { const batch = events.slice(i, i + BATCH_SIZE); - await applyEvents(db, batch, config); + await applyEvents(db, batch, config, { pubsub }); } // Refresh stale/missing identities for DIDs in this batch diff --git a/src/core/persistent.ts b/src/core/persistent.ts index 0574656..5eb48a2 100644 --- a/src/core/persistent.ts +++ b/src/core/persistent.ts @@ -15,6 +15,9 @@ export interface PersistentIngestOptions { /** Override subscription creation for testing */ createSubscription?: (cursor: number | null) => JetstreamSubscription; logger?: Logger; + /** Publish `collection:` / `actor:` events for each applied + * public record. Usually supplied by the Contrail instance. */ + pubsub?: import("./realtime/types").PubSub; } function getLogger(config: ContrailConfig, options?: PersistentIngestOptions): Logger { @@ -68,6 +71,7 @@ export async function runPersistent( state, log, createSubscription: options?.createSubscription, + pubsub: options?.pubsub, }); reconnectAttempts = 0; } catch (err) { @@ -93,6 +97,7 @@ interface StreamOptions { state: IngestState; log: Logger; createSubscription?: (cursor: number | null) => any; + pubsub?: import("./realtime/types").PubSub; } async function streamAndFlush( @@ -132,7 +137,7 @@ async function streamAndFlush( resetFlushTimer(); try { - await applyEvents(db, batch, config); + await applyEvents(db, batch, config, { pubsub: opts.pubsub }); const lastTimeUs = Math.max(...batch.map((e) => e.time_us)); await saveCursor(db, lastTimeUs); diff --git a/src/core/realtime/publishing-adapter.ts b/src/core/realtime/publishing-adapter.ts index e96a226..07cbc00 100644 --- a/src/core/realtime/publishing-adapter.ts +++ b/src/core/realtime/publishing-adapter.ts @@ -68,7 +68,7 @@ export function wrapWithPublishing( await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ topic, kind: "member.added", - payload: { spaceUri, did }, + payload: { space: spaceUri, did }, ts: now, })); }, @@ -80,7 +80,7 @@ export function wrapWithPublishing( await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ topic, kind: "member.removed", - payload: { spaceUri, did }, + payload: { space: spaceUri, did }, ts: now, })); }, @@ -94,7 +94,7 @@ export function wrapWithPublishing( await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ topic, kind: "member.added", - payload: { spaceUri, did }, + payload: { space: spaceUri, did }, ts: now, })); } @@ -102,7 +102,7 @@ export function wrapWithPublishing( await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ topic, kind: "member.removed", - payload: { spaceUri, did }, + payload: { space: spaceUri, did }, ts: now, })); } @@ -112,17 +112,22 @@ export function wrapWithPublishing( await inner.putRecord(record); const owner = await ownerOf(record.spaceUri); const now = Date.now(); + // Space records use ms timestamps; listRecords surface uses microseconds + // (time_us). Convert here so subscribers can render a row identically. + const time_us = record.createdAt * 1000; + const uri = `at://${record.authorDid}/${record.collection}/${record.rkey}`; await publishSpaceAndCommunity(record.spaceUri, owner, (topic) => ({ topic, kind: "record.created", payload: { - spaceUri: record.spaceUri, + uri, + did: record.authorDid, collection: record.collection, - authorDid: record.authorDid, rkey: record.rkey, cid: record.cid, record: record.record, - createdAt: record.createdAt, + time_us, + space: record.spaceUri, }, ts: now, })); @@ -132,10 +137,11 @@ export function wrapWithPublishing( await inner.deleteRecord(spaceUri, collection, authorDid, rkey); const owner = await ownerOf(spaceUri); const now = Date.now(); + const uri = `at://${authorDid}/${collection}/${rkey}`; await publishSpaceAndCommunity(spaceUri, owner, (topic) => ({ topic, kind: "record.deleted", - payload: { spaceUri, collection, authorDid, rkey }, + payload: { uri, did: authorDid, collection, rkey, space: spaceUri }, ts: now, })); }, diff --git a/src/core/realtime/query-filter.ts b/src/core/realtime/query-filter.ts index 6806c45..f332097 100644 --- a/src/core/realtime/query-filter.ts +++ b/src/core/realtime/query-filter.ts @@ -70,9 +70,9 @@ export function translateForQuery( const spec = sub.querySpec; if (!spec) return null; if (event.kind !== "record.created" && event.kind !== "record.deleted") return []; - if (event.payload.spaceUri !== spec.spaceUri) return []; + if (event.payload.space !== spec.spaceUri) return []; - const primaryUri = `at://${event.payload.authorDid}/${event.payload.collection}/${event.payload.rkey}`; + const primaryUri = `at://${event.payload.did}/${event.payload.collection}/${event.payload.rkey}`; if (event.payload.collection === spec.collection) { if (event.kind === "record.created") { @@ -82,7 +82,7 @@ export function translateForQuery( data: { record: { uri: primaryUri, - did: event.payload.authorDid, + did: event.payload.did, rkey: event.payload.rkey, collection: event.payload.collection, cid: event.payload.cid, @@ -100,7 +100,7 @@ export function translateForQuery( kind: "record.deleted", data: { uri: primaryUri, - did: event.payload.authorDid, + did: event.payload.did, rkey: event.payload.rkey } } @@ -125,7 +125,7 @@ export function translateForQuery( relation: relName, child: { uri: primaryUri, - did: event.payload.authorDid, + did: event.payload.did, rkey: event.payload.rkey, collection: event.payload.collection, cid: event.payload.cid, @@ -145,7 +145,7 @@ export function translateForQuery( parentUri: info.parentUri, relation: relName, childRkey: event.payload.rkey, - childDid: event.payload.authorDid + childDid: event.payload.did } } ]; diff --git a/src/core/realtime/resolve.ts b/src/core/realtime/resolve.ts index 1499faa..f8a0ab4 100644 --- a/src/core/realtime/resolve.ts +++ b/src/core/realtime/resolve.ts @@ -17,7 +17,10 @@ import { resolveReachableSpaces } from "../community/acl"; import { spaceTopic, parseCommunityTopic, parseSpaceTopic } from "./types"; export interface TopicResolutionContext { - spaces: StorageAdapter; + /** May be null when the deployment has no spaces module — in that case + * `space:` and `community:` topics are NotSupported. Public topics + * (`collection:`, `actor:`) still resolve. */ + spaces: StorageAdapter | null; /** May be null if the community module is not enabled. */ community: CommunityAdapter | null; } @@ -41,6 +44,9 @@ export async function resolveTopicForCaller( // space: const spaceUri = parseSpaceTopic(rawTopic); if (spaceUri) { + if (!ctx.spaces) { + return { ok: false, error: "NotSupported", reason: "spaces-module-disabled" }; + } const space = await ctx.spaces.getSpace(spaceUri); if (!space) return { ok: false, error: "NotFound", reason: "space-not-found" }; if (space.ownerDid === callerDid) return { ok: true, topics: [rawTopic] }; @@ -52,7 +58,7 @@ export async function resolveTopicForCaller( // community: const communityDid = parseCommunityTopic(rawTopic); if (communityDid) { - if (!ctx.community) { + if (!ctx.community || !ctx.spaces) { return { ok: false, error: "NotSupported", reason: "community-module-disabled" }; } const row = await ctx.community.getCommunity(communityDid); @@ -72,18 +78,15 @@ export async function resolveTopicForCaller( return { ok: true, topics }; } - // actor: — self-only in v1. + // actor: — public stream of records authored by this DID. + // Any caller can subscribe (parallels listRecords with an `actor` filter). if (rawTopic.startsWith("actor:")) { - const did = rawTopic.slice("actor:".length); - if (did !== callerDid) { - return { ok: false, error: "Forbidden", reason: "actor-self-only" }; - } return { ok: true, topics: [rawTopic] }; } - // collection: — public firehose, not exposed in v1. + // collection: — public firehose for this collection. if (rawTopic.startsWith("collection:")) { - return { ok: false, error: "NotSupported", reason: "collection-firehose-disabled" }; + return { ok: true, topics: [rawTopic] }; } return { ok: false, error: "InvalidRequest", reason: "unknown-topic" }; diff --git a/src/core/realtime/router.ts b/src/core/realtime/router.ts index a6448ef..2726226 100644 --- a/src/core/realtime/router.ts +++ b/src/core/realtime/router.ts @@ -15,14 +15,21 @@ import type { PubSub, RealtimeEvent } from "./types"; import { DEFAULT_TICKET_TTL_MS, DEFAULT_KEEPALIVE_MS } from "./types"; export interface RealtimeRoutesOptions { - /** Required: auth middleware for `.realtime.ticket` (JWT required). - * The subscribe endpoint uses the *ticket* path primarily, so it does not - * require this middleware — but bots can subscribe with Authorization - * header, in which case this middleware is used as a fallback. */ - authMiddleware: MiddlewareHandler; + /** Auth middleware for `.realtime.ticket` and for JWT-based bot + * subscriptions to private topics. Null when no JWT verifier is available + * (deployments without a spaces config) — in that case, private-topic + * subscribe paths return NotSupported and public topics still work without + * auth. */ + authMiddleware: MiddlewareHandler | null; pubsub?: PubSub; } +/** Public topics: subscribable without any auth. Mirrors listRecords + * semantics — no JWT means "public records only". */ +function isPublicTopic(topic: string): boolean { + return topic.startsWith("collection:") || topic.startsWith("actor:"); +} + /** WebSocketPair exists on Cloudflare Workers; on Node/Bun it's absent. * When absent, a platform-provided WebSocket accept hook is used instead. */ interface WebSocketPairCtor { @@ -32,7 +39,7 @@ interface WebSocketPairCtor { export function registerRealtimeRoutes( app: Hono, config: ContrailConfig, - spaces: StorageAdapter, + spaces: StorageAdapter | null, community: CommunityAdapter | null, options: RealtimeRoutesOptions ): void { @@ -47,36 +54,51 @@ export function registerRealtimeRoutes( const NS = `${config.namespace}.realtime`; // POST /.realtime.ticket — { topic } → { ticket, topics, expiresAt } - app.post(`/xrpc/${NS}.ticket`, options.authMiddleware, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as { topic?: string } | null; - if (!body?.topic) { - return c.json({ error: "InvalidRequest", message: "topic required" }, 400); - } - const resolved = await resolveTopicForCaller(body.topic, sa.issuer, { spaces, community }); - if (!resolved.ok) { - const status = resolved.error === "NotFound" ? 404 : resolved.error === "Forbidden" ? 403 : 400; - return c.json({ error: resolved.error, reason: resolved.reason }, status); - } - const ticket = await signer.sign({ - topics: resolved.topics, - did: sa.issuer, - ttlMs: ticketTtl, - }); - return c.json({ - ticket, - topics: resolved.topics, - expiresAt: Date.now() + ticketTtl, + // Ticket-minting exists so browsers (which can't set Authorization on + // EventSource) can subscribe to *private* topics. Public topics + // (collection:, actor:) don't need tickets — subscribe with `?topic=` directly. + if (options.authMiddleware) { + const authMw = options.authMiddleware; + app.post(`/xrpc/${NS}.ticket`, authMw, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as { topic?: string } | null; + if (!body?.topic) { + return c.json({ error: "InvalidRequest", message: "topic required" }, 400); + } + const resolved = await resolveTopicForCaller(body.topic, sa.issuer, { spaces, community }); + if (!resolved.ok) { + const status = resolved.error === "NotFound" ? 404 : resolved.error === "Forbidden" ? 403 : 400; + return c.json({ error: resolved.error, reason: resolved.reason }, status); + } + const ticket = await signer.sign({ + topics: resolved.topics, + did: sa.issuer, + ttlMs: ticketTtl, + }); + return c.json({ + ticket, + topics: resolved.topics, + expiresAt: Date.now() + ticketTtl, + }); }); - }); + } - // GET /.realtime.subscribe — SSE or WS, driven by ?ticket= or JWT. + // GET /.realtime.subscribe — SSE or WS. + // + // Three access paths, all land on the same stream: + // - `?topic=collection:` or `?topic=actor:` — *public*, no auth. + // Mirrors listRecords semantics. + // - `?ticket=` — presented by browsers, minted via `.ticket` after + // a JWT-authenticated call. Only used for private topics. + // - `Authorization: Bearer ` + `?topic=space:` — server-side + // bots can skip the ticket dance and go straight to subscribe. app.get(`/xrpc/${NS}.subscribe`, async (c) => { const url = new URL(c.req.url); const ticketParam = url.searchParams.get("ticket"); const collectionFilter = url.searchParams.get("collection"); + const topicParam = url.searchParams.get("topic"); - let callerDid: string; + let callerDid: string | null = null; let topics: string[]; if (ticketParam) { @@ -87,30 +109,41 @@ export function registerRealtimeRoutes( callerDid = payload.did; topics = payload.topics; // Optional: narrow to topics the query explicitly requests. - const requested = url.searchParams.get("topic"); - if (requested) { - if (!payload.topics.includes(requested)) { + if (topicParam) { + if (!payload.topics.includes(topicParam)) { return c.json({ error: "Forbidden", reason: "topic-not-in-ticket" }, 403); } - topics = [requested]; + topics = [topicParam]; } + } else if (topicParam && isPublicTopic(topicParam)) { + // Public subscribe — no auth required. Jetstream ingestion publishes + // record events to collection:/actor: topics directly. + topics = [topicParam]; } else { - // JWT path for server-side bots. Requires the auth middleware to have - // run and attached `serviceAuth` to the context — so we invoke it - // inline. - const runAuth = options.authMiddleware; + // JWT path for private-topic bots. If no auth middleware is available + // (deployment has no spaces config), private topics aren't offered. + if (!options.authMiddleware) { + return c.json( + { + error: "InvalidRequest", + reason: "private-topic-without-auth", + message: + "Subscribing to space:/community: topics requires a JWT verifier; only public topics (collection:, actor:) are available on this deployment.", + }, + 400 + ); + } let authed = false; - await runAuth(c, async () => { + await options.authMiddleware(c, async () => { authed = true; }); if (!authed) return c.res; // middleware already responded with 401 const sa = getAuth(c); callerDid = sa.issuer; - const topic = url.searchParams.get("topic"); - if (!topic) { + if (!topicParam) { return c.json({ error: "InvalidRequest", message: "topic required" }, 400); } - const resolved = await resolveTopicForCaller(topic, callerDid, { spaces, community }); + const resolved = await resolveTopicForCaller(topicParam, callerDid, { spaces, community }); if (!resolved.ok) { const status = resolved.error === "NotFound" ? 404 : resolved.error === "Forbidden" ? 403 : 400; return c.json({ error: resolved.error, reason: resolved.reason }, status); @@ -191,10 +224,11 @@ function anySignal(signals: AbortSignal[]): AbortSignal { /** Wrap an iterable: drop events that don't pass the collection filter (if * any), and close the outer controller as soon as we see a `member.removed` - * for the caller's own DID. */ + * for the caller's own DID. `callerDid` may be null on public subscriptions + * (anonymous) — in that case self-kick is not applicable. */ function withSelfKickAndFilter( source: AsyncIterable, - callerDid: string, + callerDid: string | null, collectionFilter: string | null, ac: AbortController ): AsyncIterable { diff --git a/src/core/realtime/types.ts b/src/core/realtime/types.ts index 4f72dfd..05a246e 100644 --- a/src/core/realtime/types.ts +++ b/src/core/realtime/types.ts @@ -3,40 +3,59 @@ /** Discriminated union of every event kind that flows through the PubSub. * * `record.created` carries the full record body so a subscriber can apply an - * insert or upsert without a follow-up `getRecord` call. `putRecord` over an - * existing `(authorDid, rkey)` publishes another `record.created` — treat it - * as upsert. */ + * insert or upsert without a follow-up `getRecord` call. Writing a new record + * to the same `(did, collection, rkey)` publishes another `record.created` — + * treat it as upsert. + * + * **Payload shape mirrors `listRecords` output** (`uri`, `did`, `space?`, + * `time_us`), so a subscriber can render a live row the same way it renders + * a fetched row. + * + * **Publisher/topic matrix (intentional trust split):** + * - `collection:` and `actor:` carry *public* record events only + * (from jetstream ingestion) — no `space`. + * - `space:` and `community:` carry *space* events — `space` is + * always set. Never cross-published to public topics (privacy). */ export type RealtimeEvent = | { topic: string; kind: "record.created"; payload: { - spaceUri: string; + uri: string; + did: string; collection: string; - authorDid: string; rkey: string; cid: string | null; record: Record; - createdAt: number; + time_us: number; + /** Present only for space records; absent for public records. */ + space?: string; }; ts: number; } | { topic: string; kind: "record.deleted"; - payload: { spaceUri: string; collection: string; authorDid: string; rkey: string }; + payload: { + uri: string; + did: string; + collection: string; + rkey: string; + /** Present only for space records; absent for public records. */ + space?: string; + }; ts: number; } | { topic: string; kind: "member.added"; - payload: { spaceUri: string; did: string }; + payload: { space: string; did: string }; ts: number; } | { topic: string; kind: "member.removed"; - payload: { spaceUri: string; did: string }; + payload: { space: string; did: string }; ts: number; }; diff --git a/src/core/router/collection.ts b/src/core/router/collection.ts index 5c637c8..d6b4968 100644 --- a/src/core/router/collection.ts +++ b/src/core/router/collection.ts @@ -18,7 +18,7 @@ import type { FormattedRecord } from "./helpers"; import { formatRecord, parseIntParam, fieldToParam } from "./helpers"; import { verifyServiceAuthRequest, extractInviteToken, checkInviteReadGrant } from "../spaces/auth"; import { checkAccess } from "../spaces/acl"; -import { hashInviteToken } from "../spaces/invite-token"; +import { hashInviteToken } from "../invite/token"; import type { SpacesContext } from "."; import type { Nsid } from "@atcute/lexicons"; import type { RealtimeEvent } from "../realtime/types"; @@ -27,8 +27,7 @@ import { spaceTopic } from "../realtime/types"; import type { SubscriberQuerySpec } from "../realtime/durable-object"; import { DurableObjectPubSub } from "../realtime/durable-object"; import { TicketSigner, type TicketQuerySpec } from "../realtime/ticket"; -import { parseHydrateParams } from "./hydrate"; -import { getRelationField, getNestedValue, nsidForShortName } from "../types"; +import { getRelationField, getNestedValue } from "../types"; /** Shared implementation of the watchRecords snapshot+live loop. Called by * both transport branches (SSE and Worker-terminated WS). The caller owns @@ -73,8 +72,7 @@ async function runQueryStream(opts: { const parentDids = new Set(); const childToParent = new Map(); - const primaryUri = (payload: { authorDid: string; collection: string; rkey: string }) => - `at://${payload.authorDid}/${payload.collection}/${payload.rkey}`; + const primaryUri = (payload: { uri: string }) => payload.uri; const handleChildEvent = (event: RealtimeEvent) => { if (!trackHydration) return; @@ -82,7 +80,7 @@ async function runQueryStream(opts: { const meta = childCollectionMap.get(event.payload.collection); if (!meta) return; if (!(hydrateSpec.relations as Record)[meta.relName]) return; - if (event.payload.spaceUri !== spaceUri) return; + if (event.payload.space !== spaceUri) return; if (event.kind === "record.created") { const matched = getNestedValue(event.payload.record, meta.matchField); @@ -105,7 +103,7 @@ async function runQueryStream(opts: { relation: meta.relName, child: { uri: primaryUri(event.payload), - did: event.payload.authorDid, + did: event.payload.did, rkey: event.payload.rkey, collection: event.payload.collection, cid: event.payload.cid, @@ -121,7 +119,7 @@ async function runQueryStream(opts: { parentUri: info.parentUri, relation: info.relName, childRkey: event.payload.rkey, - childDid: event.payload.authorDid + childDid: event.payload.did }); } }; @@ -134,7 +132,7 @@ async function runQueryStream(opts: { return; } if (event.kind !== "record.created" && event.kind !== "record.deleted") return; - if (event.payload.spaceUri !== spaceUri) return; + if (event.payload.space !== spaceUri) return; if (event.payload.collection !== colNsid) { handleChildEvent(event); @@ -145,11 +143,11 @@ async function runQueryStream(opts: { const uri = primaryUri(event.payload); if (event.kind === "record.created") { parentUris.add(uri); - parentDids.add(event.payload.authorDid); + parentDids.add(event.payload.did); send("record.created", { record: { uri, - did: event.payload.authorDid, + did: event.payload.did, rkey: event.payload.rkey, collection: event.payload.collection, cid: event.payload.cid, @@ -163,7 +161,7 @@ async function runQueryStream(opts: { parentUris.delete(uri); send("record.deleted", { uri, - did: event.payload.authorDid, + did: event.payload.did, rkey: event.payload.rkey }); } diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 35ef535..74f0499 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -89,22 +89,27 @@ export function createApp( } : null; - // Realtime pubsub is built up-front so the publishing decorator can reference - // it. The subscribe endpoint uses the same instance. + // Realtime pubsub is built whenever realtime is configured — independent of + // spaces. With spaces, the spaces adapter is wrapped so private record/member + // events publish to space:/community: topics. Without spaces, only public + // topics (collection:/actor:) see traffic — those are published from + // applyEvents (jetstream ingestion), not from here. let realtimePubsub: PubSub | null = null; - if (config.realtime && spacesCtx) { + if (config.realtime) { realtimePubsub = options.realtime?.pubsub ?? config.realtime.pubsub ?? new InMemoryPubSub({ queueBound: config.realtime.queueBound, }); - const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; - const isCommunityDid = communityAdapter - ? cachedIsCommunityDid(communityAdapter) - : undefined; - spacesCtx = { - ...spacesCtx, - adapter: wrapWithPublishing(spacesCtx.adapter, realtimePubsub, { isCommunityDid }), - }; + if (spacesCtx) { + const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; + const isCommunityDid = communityAdapter + ? cachedIsCommunityDid(communityAdapter) + : undefined; + spacesCtx = { + ...spacesCtx, + adapter: wrapWithPublishing(spacesCtx.adapter, realtimePubsub, { isCommunityDid }), + }; + } } registerAdminRoutes(app, db, config); @@ -130,13 +135,18 @@ export function createApp( ); } - if (config.realtime && spacesCtx && realtimePubsub) { - const authMiddleware = - options.realtime?.authMiddleware ?? - options.spaces?.authMiddleware ?? - createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); + if (config.realtime && realtimePubsub) { + // The ticket endpoint still needs a JWT verifier — but that verifier only + // exists when spaces is configured. Without spaces, private-topic ticket + // minting simply isn't offered; public subscriptions (collection:/actor:) + // require no auth and still work. + const authMiddleware = spacesCtx + ? options.realtime?.authMiddleware ?? + options.spaces?.authMiddleware ?? + createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }) + : null; const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; - registerRealtimeRoutes(app, config, spacesCtx.adapter, communityAdapter, { + registerRealtimeRoutes(app, config, spacesCtx?.adapter ?? null, communityAdapter, { authMiddleware, pubsub: realtimePubsub, }); diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index 6027d06..4fef67a 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -11,7 +11,7 @@ import { verifyServiceAuthRequest, } from "./auth"; import { nextTid } from "./tid"; -import { generateInviteToken, hashInviteToken } from "./invite-token"; +import { hashInviteToken, mintInviteToken } from "../invite/token"; import { buildSpaceUri } from "./uri"; import { DEFAULT_BLOB_MAX_SIZE, @@ -87,9 +87,11 @@ export function registerSpacesRoutes( /** Space endpoints are emitted per-deployment under the configured namespace; * the deployment owns and publishes its own lexicons. The library ships - * templates at `lexicons/tools/atmo/space/*` that the generator instantiates - * under `.space.*`. */ + * templates at `spaces-lexicon-templates/*` that the generator instantiates + * under `.space.*` (spec-aligned) and `.spaceExt.*` (contrail + * extras — invites, whoami — that the permissioned-data spec doesn't cover). */ const SPACE = `${config.namespace}.space`; + const SPACE_EXT = `${config.namespace}.spaceExt`; // Read endpoints app.get(`/xrpc/${SPACE}.listSpaces`, auth, async (c) => { @@ -484,8 +486,8 @@ export function registerSpacesRoutes( return c.json({ space: publicSpaceView(space, true) }); }); - // Invites - app.post(`/xrpc/${SPACE}.invite.create`, auth, async (c) => { + // Invites (contrail extras — emitted under .spaceExt.invite.*) + app.post(`/xrpc/${SPACE_EXT}.invite.create`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => null)) as | { @@ -509,8 +511,7 @@ export function registerSpacesRoutes( return c.json({ error: "Forbidden", reason: "not-owner" }, 403); } - const token = generateInviteToken(); - const tokenHash = await hashInviteToken(token); + const { token, tokenHash } = await mintInviteToken(); const invite = await adapter.createInvite({ spaceUri: body.spaceUri, tokenHash, @@ -523,7 +524,7 @@ export function registerSpacesRoutes( return c.json({ token, invite: publicInviteView(invite) }); }); - app.post(`/xrpc/${SPACE}.invite.redeem`, auth, async (c) => { + app.post(`/xrpc/${SPACE_EXT}.invite.redeem`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => null)) as { token?: string } | null; if (!body?.token) { @@ -538,7 +539,7 @@ export function registerSpacesRoutes( return c.json({ spaceUri: invite.spaceUri }); }); - app.get(`/xrpc/${SPACE}.invite.list`, auth, async (c) => { + app.get(`/xrpc/${SPACE_EXT}.invite.list`, auth, async (c) => { const sa = getAuth(c); const spaceUri = c.req.query("spaceUri"); if (!spaceUri) return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); @@ -552,7 +553,7 @@ export function registerSpacesRoutes( return c.json({ invites: invites.map(publicInviteView) }); }); - app.post(`/xrpc/${SPACE}.invite.revoke`, auth, async (c) => { + app.post(`/xrpc/${SPACE_EXT}.invite.revoke`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => null)) as | { spaceUri?: string; tokenHash?: string } @@ -624,7 +625,7 @@ export function registerSpacesRoutes( return c.json({ ok: true }); }); - app.get(`/xrpc/${SPACE}.whoami`, auth, async (c) => { + app.get(`/xrpc/${SPACE_EXT}.whoami`, auth, async (c) => { const sa = getAuth(c); const spaceUri = c.req.query("spaceUri"); if (!spaceUri) return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); diff --git a/src/core/types.ts b/src/core/types.ts index 688b89c..e45f31e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -384,6 +384,12 @@ export function validateConfig(config: ContrailConfig): void { } } } + + if (config.community && !config.spaces) { + throw new Error( + "Invalid config: `community` requires `spaces`. Community-owned spaces reuse the spaces storage adapter." + ); + } } // Helpers diff --git a/src/generate.ts b/src/generate.ts index c68ea87..2b43b2f 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -769,7 +769,10 @@ export function generateLexicons(options: GenerateOptions): Record.space.* --- + // --- Spaces: instantiate library templates under .space.* and + // .spaceExt.* (contrail-specific extras — invites, whoami). + // Splitting the namespace keeps the `space.*` surface aligned with the + // permissioned-data spec; extras move when the spec does not. --- if (config.spaces) { log("Generating space endpoints..."); @@ -777,17 +780,29 @@ export function generateLexicons(options: GenerateOptions): Record - id.startsWith("tools.atmo.space") ? id.replace(/^tools\.atmo\.space/, `${ns}.space`) : id; + // Order matters: more specific prefix first so `spaceExt` isn't matched + // by the `space` rule. + const prefixes: Array<[string, string]> = [ + ["tools.atmo.spaceExt", `${ns}.spaceExt`], + ["tools.atmo.space", `${ns}.space`], + ]; + const templateIdRe = /^tools\.atmo\.space(Ext)?(\.[A-Za-z0-9.]+)?$/; + const idReplace = (id: string) => { + for (const [from, to] of prefixes) { + if (id === from || id.startsWith(from + ".")) { + return to + id.slice(from.length); + } + } + return id; + }; const rewriteRefs = (obj: any): any => { if (Array.isArray(obj)) return obj.map(rewriteRefs); if (obj && typeof obj === "object") { const out: any = {}; for (const [k, v] of Object.entries(obj)) { - if (k === "ref" && typeof v === "string" && v.startsWith("tools.atmo.space")) { - out[k] = v.replace(/^tools\.atmo\.space/, `${ns}.space`); + if (k === "ref" && typeof v === "string") { + out[k] = idReplace(v); } else if (k === "id" && typeof v === "string" && templateIdRe.test(v)) { out[k] = idReplace(v); } else { diff --git a/src/index.ts b/src/index.ts index 404f3b4..dbd672f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,7 +50,7 @@ export type { } from "./core/spaces/types"; export { HostedAdapter } from "./core/spaces/adapter"; export { nextTid } from "./core/spaces/tid"; -export { generateInviteToken, hashInviteToken } from "./core/spaces/invite-token"; +export { generateInviteToken, hashInviteToken, mintInviteToken } from "./core/invite"; export { MemoryBlobAdapter, R2BlobAdapter, diff --git a/src/server.ts b/src/server.ts index b8b03eb..ae4f47f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,34 +1,29 @@ import type { Contrail } from "./contrail"; import type { Database } from "./core/types"; -import { createApp } from "./core/router"; /** * Create an HTTP handler from a Contrail instance. - * Returns a standard (Request, db?, spacesDb?) => Promise function. * - * Usage: + * Thin wrapper over `contrail.handler()` that accepts per-request DB overrides + * (useful on Cloudflare Workers where the DB binding lives on the request env). + * * const handle = createHandler(contrail); * // SvelteKit: export const GET = ({ request }) => handle(request); * // Workers: return handle(request, env.DB, env.SPACES_DB); + * + * For most cases, prefer `contrail.handler()` directly when DBs are bound at + * construction time. */ export function createHandler( contrail: Contrail -): (request: Request, db?: Database, spacesDb?: Database) => Promise | Response { - // Cache the Hono app when db is bound at construction - let cachedApp: ReturnType | null = null; - +): (request: Request, db?: Database, spacesDb?: Database) => Promise { + // When no per-request DBs are provided, build the app once and reuse it. + let cached: ((request: Request) => Promise) | null = null; return (request: Request, db?: Database, spacesDb?: Database) => { - const d = db ?? (contrail as any)._db; - if (!d) throw new Error("No database provided. Pass db to Contrail constructor or to handler."); - - const sd = spacesDb ?? (contrail as any)._spacesDb; - - // If db is the same bound instance, reuse the Hono app - if (!db && !cachedApp) { - cachedApp = createApp(d, contrail.config, { spacesDb: sd }); + if (db || spacesDb) { + return contrail.handler({ db, spacesDb })(request); } - - const app = db ? createApp(d, contrail.config, { spacesDb: sd }) : cachedApp!; - return app.fetch(request); + cached ??= contrail.handler(); + return cached(request); }; } diff --git a/src/sync/index.ts b/src/sync/index.ts index 15e823c..2a28a37 100644 --- a/src/sync/index.ts +++ b/src/sync/index.ts @@ -37,6 +37,14 @@ export interface WatchStoreOptions { * When omitted, the connection is opened without an explicit token — * rely on cookies / bearer tokens your fetch policy sends automatically. */ fetchTicket?: () => Promise; + /** Optional: mint a short-lived bearer token (typically an atproto + * service-auth JWT scoped to `.watchRecords`). Called once + * per connect attempt; returned token is sent as `Authorization: Bearer` + * on the handshake fetch (`mode=ws`) or on the SSE connection via a + * `?auth=` query param the server doesn't use — browsers can't set + * headers on EventSource, so for SSE the app should cookie-auth or + * fall back to `fetchTicket`. */ + fetchAuthToken?: () => Promise; /** Custom compare. Default: sort by `time_us` descending (newest first), * tie-breaking by rkey. */ compareRecords?: (a: WatchRecord, b: WatchRecord) => number; @@ -97,6 +105,12 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { let error: Error | null = null; const listeners = new Set<(state: WatchStoreState) => void>(); + // Per-snapshot reconcile: during a snapshot we track which keys were + // included, and on snapshot.end we evict any stale keys the previous + // snapshot had but this one didn't. Keeps data visible across reconnects + // and prevents stale records accumulating forever. + let snapshotSeen: Set | null = null; + let es: EventSource | null = null; let ws: WebSocket | null = null; let started = false; @@ -127,7 +141,9 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { }; const applySnapshotRecord = (record: WatchRecord) => { - byKey.set(key(record), record); + const k = key(record); + byKey.set(k, record); + snapshotSeen?.add(k); }; const applyCreated = (record: WatchRecord) => { @@ -199,14 +215,28 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { case "snapshot.start": setStatus("snapshot"); backoffMs = 1000; + // Begin reconcile pass. Stale records from the previous session + // stay visible until snapshot.end replaces them. + snapshotSeen = new Set(); break; case "snapshot.record": applySnapshotRecord(msg.data.record); break; - case "snapshot.end": + case "snapshot.end": { + // Evict anything we had before this snapshot that the server + // didn't re-send. Preserves continuity across reconnects + // while still dropping records that were deleted while we + // were disconnected. + if (snapshotSeen) { + for (const k of Array.from(byKey.keys())) { + if (!snapshotSeen.has(k)) byKey.delete(k); + } + snapshotSeen = null; + } resort(); setStatus("live"); break; + } case "record.created": applyCreated(msg.data.record); break; @@ -282,7 +312,12 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { }; const openWs = async () => { - // Step 1: snapshot + handshake. + // Step 1: snapshot + handshake. Authenticated by Bearer token from + // `fetchAuthToken` (an atproto service-auth JWT — browsers get this + // from a same-origin helper that uses the OAuth session). The server + // returns a ticket bound to (did, spaceUri, querySpec); we pass that + // back embedded in the wsUrl on the WS upgrade so the WS itself + // doesn't need a JWT (EventSource/WebSocket can't set headers). let snapshotUrl = options.url; const sep = snapshotUrl.includes("?") ? "&" : "?"; snapshotUrl += `${sep}mode=ws`; @@ -290,7 +325,12 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { const ticket = await options.fetchTicket(); snapshotUrl += `&ticket=${encodeURIComponent(ticket)}`; } - const res = await fetch(snapshotUrl, { headers: { accept: "application/json" } }); + const headers: Record = { accept: "application/json" }; + if (options.fetchAuthToken) { + const token = await options.fetchAuthToken(); + headers.authorization = `Bearer ${token}`; + } + const res = await fetch(snapshotUrl, { headers }); if (!res.ok) throw new Error(`snapshot fetch failed (${res.status})`); const handshake = (await res.json()) as { transport: "ws"; @@ -311,8 +351,15 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { resort(); backoffMs = 1000; - // Step 2: open the WS. `wsUrl` is a relative path from the server. - const wsHref = new URL(handshake.wsUrl, options.url); + // Step 2: open the WS. `wsUrl` is a relative path from the server; + // resolve it against the page origin (not against options.url — both + // are relative and `new URL(relative, relative)` throws, which + // previously manifested as the connection indicator being stuck on + // "connecting" while the engine re-fetched snapshots in a tight + // loop). The server embeds the handshake ticket in it when issued. + const g = globalThis as { location?: { origin?: string } }; + const base = g.location?.origin ?? "http://localhost"; + const wsHref = new URL(handshake.wsUrl, base); wsHref.protocol = wsHref.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket(wsHref.toString()); ws = socket; @@ -365,8 +412,8 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { backoffMs = Math.min(backoffMs * 2, 30_000); setTimeout(() => { if (stopped) return; - byKey.clear(); - sorted = []; + // Keep existing records visible; `snapshot.end` will reconcile + // away anything that disappeared while we were offline. void openOnce(); }, delay); }; diff --git a/tests/community-delegation.test.ts b/tests/community-delegation.test.ts index 7466dac..bee3af4 100644 --- a/tests/community-delegation.test.ts +++ b/tests/community-delegation.test.ts @@ -125,7 +125,7 @@ async function grant(app: Hono, caller: string, spaceUri: string, subject: any, } async function whoamiLevel(app: Hono, caller: string, spaceUri: string): Promise { - const res = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, caller); + const res = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, caller); expect(res.status).toBe(200); return ((await res.json()) as any).accessLevel; } diff --git a/tests/community-e2e.test.ts b/tests/community-e2e.test.ts index 5203871..f9d5338 100644 --- a/tests/community-e2e.test.ts +++ b/tests/community-e2e.test.ts @@ -136,13 +136,13 @@ describe("community e2e — stage 1", () => { // whoami in both reserved spaces → owner for (const uri of [adminUri, publishersUri]) { - const r = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(uri)}`, ALICE); + const r = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(uri)}`, ALICE); expect(r.status).toBe(200); expect(((await r.json()) as any).accessLevel).toBe("owner"); } // Bob isn't in either - const r = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(adminUri)}`, BOB); + const r = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(adminUri)}`, BOB); expect(r.status).toBe(200); expect(((await r.json()) as any).accessLevel).toBe(null); }); @@ -255,7 +255,7 @@ describe("community e2e — stage 1", () => { const level = await call( app, "GET", - `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, + `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, BOB ); expect(((await level.json()) as any).accessLevel).toBe(null); diff --git a/tests/community-mint.test.ts b/tests/community-mint.test.ts index 96922a6..b573dfc 100644 --- a/tests/community-mint.test.ts +++ b/tests/community-mint.test.ts @@ -150,7 +150,7 @@ describe("community.mint — stage 4", () => { // Reserved spaces exist with the caller as owner. const adminUri = `at://${body.communityDid}/tools.atmo.event.space/$admin`; - const whoami = await call(app, "GET", `/xrpc/test.comm.community.whoami?spaceUri=${encodeURIComponent(adminUri)}`, ALICE); + const whoami = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(adminUri)}`, ALICE); expect(((await whoami.json()) as any).accessLevel).toBe("owner"); }); diff --git a/tests/contrail-entry.test.ts b/tests/contrail-entry.test.ts new file mode 100644 index 0000000..f447a00 --- /dev/null +++ b/tests/contrail-entry.test.ts @@ -0,0 +1,95 @@ +/** Sanity-checks for the unified Contrail entry point: + * - `contrail.app()` returns a Hono instance with routes wired + * - `contrail.handler()` is a fetch-compatible function + * - the shared pubsub is threaded through so subscribing via the app + * receives events published via ingest-path helpers on the same instance. */ + +import { describe, it, expect } from "vitest"; +import { Contrail } from "../src/contrail"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { applyEvents } from "../src/core/db/records"; +import type { IngestEvent } from "../src/core/types"; + +const MSG_NSID = "app.event.message"; + +describe("Contrail.app() / Contrail.handler()", () => { + it("handler serves the health endpoint", async () => { + const db = createSqliteDatabase(":memory:"); + const contrail = new Contrail({ + namespace: "test.entry", + collections: { message: { collection: MSG_NSID } }, + db, + }); + await contrail.init(); + + const handle = contrail.handler(); + const res = await handle(new Request("http://localhost/health")); + expect(res.status).toBe(200); + expect(((await res.json()) as any).status).toBe("ok"); + }); + + it("shares its pubsub: events from applyEvents reach subscribers of contrail.app()", async () => { + const db = createSqliteDatabase(":memory:"); + const contrail = new Contrail({ + namespace: "test.entry", + collections: { message: { collection: MSG_NSID } }, + realtime: { ticketSecret: new Uint8Array(32).fill(1), keepaliveMs: 60_000 }, + db, + }); + await contrail.init(); + expect(contrail.pubsub).toBeTruthy(); + + const app = contrail.app(); + const ac = new AbortController(); + const res = await app.fetch( + new Request( + `http://localhost/xrpc/test.entry.realtime.subscribe?topic=${encodeURIComponent("collection:" + MSG_NSID)}`, + { signal: ac.signal } + ) + ); + expect(res.status).toBe(200); + const reader = res.body!.getReader(); + + // Use the same pubsub via the exposed getter — this is the promise + // of "one Contrail, one pubsub, one app". + const e: IngestEvent = { + uri: `at://did:plc:a/${MSG_NSID}/1`, + did: "did:plc:a", + time_us: 1, + collection: MSG_NSID, + operation: "create", + rkey: "1", + cid: "bafy", + record: JSON.stringify({ text: "shared" }), + indexed_at: Date.now() * 1000, + }; + await applyEvents(db, [e], contrail.config, { pubsub: contrail.pubsub ?? undefined }); + + // SSE frames separated by "\n\n". Skip any that don't carry a data: line + // (comments / keepalives). + const decoder = new TextDecoder(); + let buf = ""; + let payload: any = null; + while (!payload) { + const sep = buf.indexOf("\n\n"); + if (sep < 0) { + const { value, done } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + continue; + } + const frame = buf.slice(0, sep); + buf = buf.slice(sep + 2); + const dataLine = frame.split("\n").find((l) => l.startsWith("data:")); + if (dataLine) { + try { payload = JSON.parse(dataLine.slice(5).trim()); } catch {} + } + } + ac.abort(); + reader.cancel().catch(() => {}); + + expect(payload).toBeTruthy(); + expect(payload.kind).toBe("record.created"); + expect(payload.payload.did).toBe("did:plc:a"); + }); +}); diff --git a/tests/realtime-durable-object.test.ts b/tests/realtime-durable-object.test.ts index 0c79e34..3953e52 100644 --- a/tests/realtime-durable-object.test.ts +++ b/tests/realtime-durable-object.test.ts @@ -52,13 +52,14 @@ function mkEvent(overrides: Partial = {}): RealtimeEvent { topic: "space:at://x/y/z", kind: "record.created", payload: { - spaceUri: "at://x/y/z", + uri: "at://did:plc:x/c/r", + did: "did:plc:x", collection: "c", - authorDid: "did:plc:x", rkey: "r", cid: null, record: {}, - createdAt: 1, + time_us: 1, + space: "at://x/y/z", }, ts: 1, ...(overrides as any), @@ -96,7 +97,7 @@ describe("RealtimePubSubDO — publish/fanout", () => { doInstance.publishEvent({ topic: "space:x", kind: "member.removed", - payload: { spaceUri: "at://x/y/z", did: "did:plc:kickme" }, + payload: { space: "at://x/y/z", did: "did:plc:kickme" }, ts: 1, }); @@ -141,7 +142,7 @@ describe("RealtimePubSubDO — publish/fanout", () => { doInstance.publishEvent({ topic: "space:x", kind: "member.removed", - payload: { spaceUri: "at://x/y/z", did: "did:plc:self" }, + payload: { space: "at://x/y/z", did: "did:plc:self" }, ts: 8, }); diff --git a/tests/realtime-e2e.test.ts b/tests/realtime-e2e.test.ts index df11f61..7a3d1ec 100644 --- a/tests/realtime-e2e.test.ts +++ b/tests/realtime-e2e.test.ts @@ -191,7 +191,7 @@ describe("realtime e2e (in-memory pubsub, SSE transport)", () => { // Fat payload: subscribers can render the new record without a follow-up fetch. expect(event.payload.record).toEqual({ text: "hello" }); expect(event.payload.collection).toBe("app.event.message"); - expect(event.payload.authorDid).toBe(ALICE); + expect(event.payload.did).toBe(ALICE); close(); }); diff --git a/tests/realtime-foundations.test.ts b/tests/realtime-foundations.test.ts index 9d814d8..c6987d6 100644 --- a/tests/realtime-foundations.test.ts +++ b/tests/realtime-foundations.test.ts @@ -11,13 +11,14 @@ function mk(topic: string, n: number): RealtimeEvent { topic, kind: "record.created", payload: { - spaceUri: "at://x/y/z", + uri: `at://did:plc:x/c/${n}`, + did: "did:plc:x", collection: "c", - authorDid: "did:plc:x", rkey: String(n), cid: null, record: {}, - createdAt: n, + time_us: n, + space: "at://x/y/z", }, ts: n, }; diff --git a/tests/realtime-public.test.ts b/tests/realtime-public.test.ts new file mode 100644 index 0000000..34b13f3 --- /dev/null +++ b/tests/realtime-public.test.ts @@ -0,0 +1,161 @@ +/** Realtime — public topics (`collection:`, `actor:`): + * - Subscribe requires no auth. + * - Events are published by `applyEvents` (the jetstream/public-record path), + * not by the spaces adapter. Spaces can be entirely absent from config. */ + +import { describe, it, expect } from "vitest"; +import { Hono } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { applyEvents } from "../src/core/db/records"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig, IngestEvent } from "../src/core/types"; +import { InMemoryPubSub } from "../src/core/realtime/in-memory"; +import type { RealtimeEvent } from "../src/core/realtime/types"; + +const REALTIME_SECRET = new Uint8Array(32).fill(9); + +const ALICE = "did:plc:alice"; +const MSG_NSID = "app.event.message"; + +function baseConfig(pubsub: InMemoryPubSub): ContrailConfig { + return { + namespace: "test.pub", + collections: { message: { collection: MSG_NSID } }, + realtime: { + ticketSecret: REALTIME_SECRET, + pubsub, + keepaliveMs: 60_000, + }, + }; +} + +/** Open SSE and return an async iterator over decoded events. */ +async function openSse(app: Hono, path: string): Promise<{ + events: AsyncIterator; + close: () => void; +}> { + const ac = new AbortController(); + const res = await app.fetch( + new Request(`http://localhost${path}`, { method: "GET", signal: ac.signal }) + ); + if (!res.ok) throw new Error(`SSE open failed: ${res.status} ${await res.text()}`); + const reader = res.body!.getReader(); + const decoder = new TextDecoder(); + let buf = ""; + const iter: AsyncIterator = { + async next() { + while (true) { + const sep = buf.indexOf("\n\n"); + if (sep >= 0) { + const frame = buf.slice(0, sep); + buf = buf.slice(sep + 2); + for (const line of frame.split("\n")) { + if (line.startsWith("data:")) { + try { return { done: false, value: JSON.parse(line.slice(5).trim()) }; } catch {} + } + } + continue; + } + const { value, done } = await reader.read(); + if (done) return { done: true, value: undefined as any }; + buf += decoder.decode(value, { stream: true }); + } + }, + }; + return { + events: iter, + close: () => { ac.abort(); reader.cancel().catch(() => {}); }, + }; +} + +describe("realtime — public topics work without spaces and without auth", () => { + it("subscribes to collection: without auth and receives jetstream events", async () => { + const db = createSqliteDatabase(":memory:"); + const pubsub = new InMemoryPubSub(); + const config = resolveConfig(baseConfig(pubsub)); + await initSchema(db, config); + const app = createApp(db, config); + + const { events, close } = await openSse( + app, + `/xrpc/test.pub.realtime.subscribe?topic=${encodeURIComponent("collection:" + MSG_NSID)}` + ); + + // Simulate a public record landing via jetstream. + const e: IngestEvent = { + uri: `at://${ALICE}/${MSG_NSID}/abc`, + did: ALICE, + time_us: 1_700_000_000_000_000, + collection: MSG_NSID, + operation: "create", + rkey: "abc", + cid: "bafytest", + record: JSON.stringify({ text: "hi" }), + indexed_at: Date.now() * 1000, + }; + await applyEvents(db, [e], config, { pubsub }); + + const next = await events.next(); + expect(next.done).toBe(false); + const event = next.value as RealtimeEvent & { kind: "record.created" }; + expect(event.kind).toBe("record.created"); + expect(event.payload.uri).toBe(e.uri); + expect(event.payload.did).toBe(ALICE); + expect(event.payload.collection).toBe(MSG_NSID); + expect(event.payload.record).toEqual({ text: "hi" }); + expect((event.payload as any).space).toBeUndefined(); + close(); + }); + + it("actor: is subscribable without auth and receives events for that DID", async () => { + const db = createSqliteDatabase(":memory:"); + const pubsub = new InMemoryPubSub(); + const config = resolveConfig(baseConfig(pubsub)); + await initSchema(db, config); + const app = createApp(db, config); + + const { events, close } = await openSse( + app, + `/xrpc/test.pub.realtime.subscribe?topic=${encodeURIComponent("actor:" + ALICE)}` + ); + + const e: IngestEvent = { + uri: `at://${ALICE}/${MSG_NSID}/xyz`, + did: ALICE, + time_us: 1_700_000_000_000_000, + collection: MSG_NSID, + operation: "create", + rkey: "xyz", + cid: "bafy", + record: JSON.stringify({ text: "from actor feed" }), + indexed_at: Date.now() * 1000, + }; + await applyEvents(db, [e], config, { pubsub }); + + const next = await events.next(); + expect(next.done).toBe(false); + const event = next.value as RealtimeEvent & { kind: "record.created" }; + expect(event.topic).toBe(`actor:${ALICE}`); + expect(event.payload.did).toBe(ALICE); + close(); + }); + + it("private topic without auth middleware returns 400 with a helpful error", async () => { + const db = createSqliteDatabase(":memory:"); + const pubsub = new InMemoryPubSub(); + const config = resolveConfig(baseConfig(pubsub)); + await initSchema(db, config); + const app = createApp(db, config); + + const res = await app.fetch( + new Request( + `http://localhost/xrpc/test.pub.realtime.subscribe?topic=${encodeURIComponent("space:at://x/y/z")}` + ) + ); + expect(res.status).toBe(400); + const body = (await res.json()) as any; + expect(body.reason).toBe("private-topic-without-auth"); + }); +}); diff --git a/tests/spaces-invites.test.ts b/tests/spaces-invites.test.ts index 22219b3..82f58f7 100644 --- a/tests/spaces-invites.test.ts +++ b/tests/spaces-invites.test.ts @@ -6,7 +6,7 @@ import { initSchema } from "../src/core/db/schema"; import { createApp } from "../src/core/router"; import { resolveConfig } from "../src/core/types"; import type { ContrailConfig } from "../src/core/types"; -import { generateInviteToken, hashInviteToken } from "../src/core/spaces/invite-token"; +import { generateInviteToken, hashInviteToken } from "../src/core/invite/token"; const ALICE = "did:plc:alice"; const BOB = "did:plc:bob"; @@ -84,14 +84,14 @@ describe("invite e2e", () => { }); it("non-owner cannot create an invite", async () => { - const res = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", BOB, { + const res = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", BOB, { spaceUri, }); expect(res.status).toBe(403); }); it("owner creates an invite and Bob redeems it to become a member", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, }); expect(create.status).toBe(200); @@ -101,7 +101,7 @@ describe("invite e2e", () => { expect(invite.spaceUri).toBe(spaceUri); expect(invite.usedCount).toBe(0); - const redeem = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", BOB, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", BOB, { token }); expect(redeem.status).toBe(200); const body = (await redeem.json()) as any; expect(body.spaceUri).toBe(spaceUri); @@ -116,58 +116,58 @@ describe("invite e2e", () => { }); it("single-use invite rejects second redemption", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, maxUses: 1, }); const { token } = (await create.json()) as any; - const first = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", BOB, { token }); + const first = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", BOB, { token }); expect(first.status).toBe(200); - const second = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); + const second = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); expect(second.status).toBe(400); const body = (await second.json()) as any; expect(body.reason).toBe("expired-revoked-or-exhausted"); }); it("expired invite rejects redemption", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, expiresAt: Date.now() - 1000, }); const { token } = (await create.json()) as any; - const res = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); + const res = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); expect(res.status).toBe(400); }); it("revoked invite rejects redemption and list filters it by default", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, }); const { token, invite } = (await create.json()) as any; - const revoke = await call(app, "POST", "/xrpc/test.spaces.space.invite.revoke", ALICE, { + const revoke = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.revoke", ALICE, { spaceUri, tokenHash: invite.tokenHash, }); expect(revoke.status).toBe(200); expect(((await revoke.json()) as any).ok).toBe(true); - const tryRedeem = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); + const tryRedeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); expect(tryRedeem.status).toBe(400); - const listActive = await call(app, "GET", `/xrpc/test.spaces.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, ALICE); + const listActive = await call(app, "GET", `/xrpc/test.spaces.spaceExt.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, ALICE); const activeHashes = ((await listActive.json()) as any).invites.map((i: any) => i.tokenHash); expect(activeHashes).not.toContain(invite.tokenHash); - const listAll = await call(app, "GET", `/xrpc/test.spaces.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}&includeRevoked=true`, ALICE); + const listAll = await call(app, "GET", `/xrpc/test.spaces.spaceExt.invite.list?spaceUri=${encodeURIComponent(spaceUri)}&includeRevoked=true`, ALICE); const allHashes = ((await listAll.json()) as any).invites.map((i: any) => i.tokenHash); expect(allHashes).toContain(invite.tokenHash); }); it("non-owner cannot list or revoke invites", async () => { - const listRes = await call(app, "GET", `/xrpc/test.spaces.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, BOB); + const listRes = await call(app, "GET", `/xrpc/test.spaces.spaceExt.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, BOB); expect(listRes.status).toBe(403); - const revokeRes = await call(app, "POST", "/xrpc/test.spaces.space.invite.revoke", BOB, { + const revokeRes = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.revoke", BOB, { spaceUri, tokenHash: "nonexistent", }); expect(revokeRes.status).toBe(403); @@ -179,7 +179,7 @@ describe("invite e2e", () => { spaceUri, collection: "app.event.message", record: { text: "hello world" }, }); - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, kind: "read", }); const { token, invite } = (await create.json()) as any; @@ -193,12 +193,12 @@ describe("invite e2e", () => { expect(body.records.length).toBeGreaterThan(0); // Read token cannot be redeemed for membership. - const redeem = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); expect(redeem.status).toBe(400); }); it("read-join token grants anonymous read AND can be redeemed for membership", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, kind: "read-join", }); const { token, invite } = (await create.json()) as any; @@ -210,16 +210,16 @@ describe("invite e2e", () => { expect(anon.status).toBe(200); // Same token can also be redeemed by a signed-in user. - const redeem = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); expect(redeem.status).toBe(200); }); it("revoked read-token rejects anonymous read", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, kind: "read", }); const { token, invite } = (await create.json()) as any; - await call(app, "POST", "/xrpc/test.spaces.space.invite.revoke", ALICE, { + await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.revoke", ALICE, { spaceUri, tokenHash: invite.tokenHash, }); const url = `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.message&inviteToken=${token}`; @@ -228,7 +228,7 @@ describe("invite e2e", () => { }); it("read-token does not grant write", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { spaceUri, kind: "read", }); const { token } = (await create.json()) as any; -- 2.51.2 From 5c8450a0a62bc6447ccc437b9681b5696ba870b2 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:34:52 +0200 Subject: [PATCH 10/18] move templates --- docs/community-spec-mapping.md | 2 +- docs/spaces-spec-mapping.md | 2 +- .../community}/adopt.json | 0 .../community}/defs.json | 0 .../community}/delete.json | 0 .../community}/deleteRecord.json | 0 .../community}/getHealth.json | 0 .../community}/invite/create.json | 0 .../community}/invite/list.json | 0 .../community}/invite/redeem.json | 0 .../community}/invite/revoke.json | 0 .../community}/list.json | 0 .../community}/mint.json | 0 .../community}/putRecord.json | 0 .../community}/reauth.json | 0 .../community}/space/create.json | 0 .../community}/space/delete.json | 0 .../community}/space/deleteRecord.json | 0 .../community}/space/grant.json | 0 .../community}/space/listMembers.json | 0 .../community}/space/putRecord.json | 0 .../community}/space/resync.json | 0 .../community}/space/revoke.json | 0 .../community}/space/setAccessLevel.json | 0 .../community}/space/whoami.json | 0 .../realtime}/subscribe.json | 0 .../realtime}/ticket.json | 0 .../spaces}/addMember.json | 0 .../spaces}/createSpace.json | 0 .../spaces}/defs.json | 0 .../spaces}/deleteRecord.json | 0 .../spaces}/ext/invite/create.json | 0 .../spaces}/ext/invite/list.json | 0 .../spaces}/ext/invite/redeem.json | 0 .../spaces}/ext/invite/revoke.json | 0 .../spaces}/ext/whoami.json | 0 .../spaces}/getBlob.json | 0 .../spaces}/getRecord.json | 0 .../spaces}/getSpace.json | 0 .../spaces}/leaveSpace.json | 0 .../spaces}/listBlobs.json | 0 .../spaces}/listMembers.json | 0 .../spaces}/listRecords.json | 0 .../spaces}/listSpaces.json | 0 .../spaces}/putRecord.json | 0 .../spaces}/removeMember.json | 0 .../spaces}/uploadBlob.json | 0 lexicons/README.md | 6 ++- package.json | 8 +--- src/core/spaces/router.ts | 2 +- src/generate.ts | 37 ++++++------------- 51 files changed, 20 insertions(+), 37 deletions(-) rename {community-lexicon-templates => lexicon-templates/community}/adopt.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/defs.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/delete.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/deleteRecord.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/getHealth.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/invite/create.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/invite/list.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/invite/redeem.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/invite/revoke.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/list.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/mint.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/putRecord.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/reauth.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/create.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/delete.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/deleteRecord.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/grant.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/listMembers.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/putRecord.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/resync.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/revoke.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/setAccessLevel.json (100%) rename {community-lexicon-templates => lexicon-templates/community}/space/whoami.json (100%) rename {realtime-lexicon-templates => lexicon-templates/realtime}/subscribe.json (100%) rename {realtime-lexicon-templates => lexicon-templates/realtime}/ticket.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/addMember.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/createSpace.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/defs.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/deleteRecord.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/ext/invite/create.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/ext/invite/list.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/ext/invite/redeem.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/ext/invite/revoke.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/ext/whoami.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/getBlob.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/getRecord.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/getSpace.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/leaveSpace.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/listBlobs.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/listMembers.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/listRecords.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/listSpaces.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/putRecord.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/removeMember.json (100%) rename {spaces-lexicon-templates => lexicon-templates/spaces}/uploadBlob.json (100%) diff --git a/docs/community-spec-mapping.md b/docs/community-spec-mapping.md index 290ee62..902528b 100644 --- a/docs/community-spec-mapping.md +++ b/docs/community-spec-mapping.md @@ -48,7 +48,7 @@ Legend: ✅ aligned · ⚠️ partial / different granularity · ❌ missing · ## XRPC surface All endpoints emitted under `.community.*` from templates -in `community-lexicon-templates/`. Distinct from `.space.*` so the +in `lexicon-templates/community/`. Distinct from `.space.*` so the user-managed spaces surface stays clean. Community-managed spaces live under `.community.space.*` so the endpoint name reveals whether membership is user-controlled or community-controlled. diff --git a/docs/spaces-spec-mapping.md b/docs/spaces-spec-mapping.md index 3368191..e06eb2f 100644 --- a/docs/spaces-spec-mapping.md +++ b/docs/spaces-spec-mapping.md @@ -36,7 +36,7 @@ flow is shipped (same story we already have for public records via jetstream). ## Endpoints All endpoints are emitted under `.space.*` from templates in -`spaces-lexicon-templates/`. +`lexicon-templates/spaces/`. ### Read - `space.listSpaces` — caller's spaces (scope=member|owner) diff --git a/community-lexicon-templates/adopt.json b/lexicon-templates/community/adopt.json similarity index 100% rename from community-lexicon-templates/adopt.json rename to lexicon-templates/community/adopt.json diff --git a/community-lexicon-templates/defs.json b/lexicon-templates/community/defs.json similarity index 100% rename from community-lexicon-templates/defs.json rename to lexicon-templates/community/defs.json diff --git a/community-lexicon-templates/delete.json b/lexicon-templates/community/delete.json similarity index 100% rename from community-lexicon-templates/delete.json rename to lexicon-templates/community/delete.json diff --git a/community-lexicon-templates/deleteRecord.json b/lexicon-templates/community/deleteRecord.json similarity index 100% rename from community-lexicon-templates/deleteRecord.json rename to lexicon-templates/community/deleteRecord.json diff --git a/community-lexicon-templates/getHealth.json b/lexicon-templates/community/getHealth.json similarity index 100% rename from community-lexicon-templates/getHealth.json rename to lexicon-templates/community/getHealth.json diff --git a/community-lexicon-templates/invite/create.json b/lexicon-templates/community/invite/create.json similarity index 100% rename from community-lexicon-templates/invite/create.json rename to lexicon-templates/community/invite/create.json diff --git a/community-lexicon-templates/invite/list.json b/lexicon-templates/community/invite/list.json similarity index 100% rename from community-lexicon-templates/invite/list.json rename to lexicon-templates/community/invite/list.json diff --git a/community-lexicon-templates/invite/redeem.json b/lexicon-templates/community/invite/redeem.json similarity index 100% rename from community-lexicon-templates/invite/redeem.json rename to lexicon-templates/community/invite/redeem.json diff --git a/community-lexicon-templates/invite/revoke.json b/lexicon-templates/community/invite/revoke.json similarity index 100% rename from community-lexicon-templates/invite/revoke.json rename to lexicon-templates/community/invite/revoke.json diff --git a/community-lexicon-templates/list.json b/lexicon-templates/community/list.json similarity index 100% rename from community-lexicon-templates/list.json rename to lexicon-templates/community/list.json diff --git a/community-lexicon-templates/mint.json b/lexicon-templates/community/mint.json similarity index 100% rename from community-lexicon-templates/mint.json rename to lexicon-templates/community/mint.json diff --git a/community-lexicon-templates/putRecord.json b/lexicon-templates/community/putRecord.json similarity index 100% rename from community-lexicon-templates/putRecord.json rename to lexicon-templates/community/putRecord.json diff --git a/community-lexicon-templates/reauth.json b/lexicon-templates/community/reauth.json similarity index 100% rename from community-lexicon-templates/reauth.json rename to lexicon-templates/community/reauth.json diff --git a/community-lexicon-templates/space/create.json b/lexicon-templates/community/space/create.json similarity index 100% rename from community-lexicon-templates/space/create.json rename to lexicon-templates/community/space/create.json diff --git a/community-lexicon-templates/space/delete.json b/lexicon-templates/community/space/delete.json similarity index 100% rename from community-lexicon-templates/space/delete.json rename to lexicon-templates/community/space/delete.json diff --git a/community-lexicon-templates/space/deleteRecord.json b/lexicon-templates/community/space/deleteRecord.json similarity index 100% rename from community-lexicon-templates/space/deleteRecord.json rename to lexicon-templates/community/space/deleteRecord.json diff --git a/community-lexicon-templates/space/grant.json b/lexicon-templates/community/space/grant.json similarity index 100% rename from community-lexicon-templates/space/grant.json rename to lexicon-templates/community/space/grant.json diff --git a/community-lexicon-templates/space/listMembers.json b/lexicon-templates/community/space/listMembers.json similarity index 100% rename from community-lexicon-templates/space/listMembers.json rename to lexicon-templates/community/space/listMembers.json diff --git a/community-lexicon-templates/space/putRecord.json b/lexicon-templates/community/space/putRecord.json similarity index 100% rename from community-lexicon-templates/space/putRecord.json rename to lexicon-templates/community/space/putRecord.json diff --git a/community-lexicon-templates/space/resync.json b/lexicon-templates/community/space/resync.json similarity index 100% rename from community-lexicon-templates/space/resync.json rename to lexicon-templates/community/space/resync.json diff --git a/community-lexicon-templates/space/revoke.json b/lexicon-templates/community/space/revoke.json similarity index 100% rename from community-lexicon-templates/space/revoke.json rename to lexicon-templates/community/space/revoke.json diff --git a/community-lexicon-templates/space/setAccessLevel.json b/lexicon-templates/community/space/setAccessLevel.json similarity index 100% rename from community-lexicon-templates/space/setAccessLevel.json rename to lexicon-templates/community/space/setAccessLevel.json diff --git a/community-lexicon-templates/space/whoami.json b/lexicon-templates/community/space/whoami.json similarity index 100% rename from community-lexicon-templates/space/whoami.json rename to lexicon-templates/community/space/whoami.json diff --git a/realtime-lexicon-templates/subscribe.json b/lexicon-templates/realtime/subscribe.json similarity index 100% rename from realtime-lexicon-templates/subscribe.json rename to lexicon-templates/realtime/subscribe.json diff --git a/realtime-lexicon-templates/ticket.json b/lexicon-templates/realtime/ticket.json similarity index 100% rename from realtime-lexicon-templates/ticket.json rename to lexicon-templates/realtime/ticket.json diff --git a/spaces-lexicon-templates/addMember.json b/lexicon-templates/spaces/addMember.json similarity index 100% rename from spaces-lexicon-templates/addMember.json rename to lexicon-templates/spaces/addMember.json diff --git a/spaces-lexicon-templates/createSpace.json b/lexicon-templates/spaces/createSpace.json similarity index 100% rename from spaces-lexicon-templates/createSpace.json rename to lexicon-templates/spaces/createSpace.json diff --git a/spaces-lexicon-templates/defs.json b/lexicon-templates/spaces/defs.json similarity index 100% rename from spaces-lexicon-templates/defs.json rename to lexicon-templates/spaces/defs.json diff --git a/spaces-lexicon-templates/deleteRecord.json b/lexicon-templates/spaces/deleteRecord.json similarity index 100% rename from spaces-lexicon-templates/deleteRecord.json rename to lexicon-templates/spaces/deleteRecord.json diff --git a/spaces-lexicon-templates/ext/invite/create.json b/lexicon-templates/spaces/ext/invite/create.json similarity index 100% rename from spaces-lexicon-templates/ext/invite/create.json rename to lexicon-templates/spaces/ext/invite/create.json diff --git a/spaces-lexicon-templates/ext/invite/list.json b/lexicon-templates/spaces/ext/invite/list.json similarity index 100% rename from spaces-lexicon-templates/ext/invite/list.json rename to lexicon-templates/spaces/ext/invite/list.json diff --git a/spaces-lexicon-templates/ext/invite/redeem.json b/lexicon-templates/spaces/ext/invite/redeem.json similarity index 100% rename from spaces-lexicon-templates/ext/invite/redeem.json rename to lexicon-templates/spaces/ext/invite/redeem.json diff --git a/spaces-lexicon-templates/ext/invite/revoke.json b/lexicon-templates/spaces/ext/invite/revoke.json similarity index 100% rename from spaces-lexicon-templates/ext/invite/revoke.json rename to lexicon-templates/spaces/ext/invite/revoke.json diff --git a/spaces-lexicon-templates/ext/whoami.json b/lexicon-templates/spaces/ext/whoami.json similarity index 100% rename from spaces-lexicon-templates/ext/whoami.json rename to lexicon-templates/spaces/ext/whoami.json diff --git a/spaces-lexicon-templates/getBlob.json b/lexicon-templates/spaces/getBlob.json similarity index 100% rename from spaces-lexicon-templates/getBlob.json rename to lexicon-templates/spaces/getBlob.json diff --git a/spaces-lexicon-templates/getRecord.json b/lexicon-templates/spaces/getRecord.json similarity index 100% rename from spaces-lexicon-templates/getRecord.json rename to lexicon-templates/spaces/getRecord.json diff --git a/spaces-lexicon-templates/getSpace.json b/lexicon-templates/spaces/getSpace.json similarity index 100% rename from spaces-lexicon-templates/getSpace.json rename to lexicon-templates/spaces/getSpace.json diff --git a/spaces-lexicon-templates/leaveSpace.json b/lexicon-templates/spaces/leaveSpace.json similarity index 100% rename from spaces-lexicon-templates/leaveSpace.json rename to lexicon-templates/spaces/leaveSpace.json diff --git a/spaces-lexicon-templates/listBlobs.json b/lexicon-templates/spaces/listBlobs.json similarity index 100% rename from spaces-lexicon-templates/listBlobs.json rename to lexicon-templates/spaces/listBlobs.json diff --git a/spaces-lexicon-templates/listMembers.json b/lexicon-templates/spaces/listMembers.json similarity index 100% rename from spaces-lexicon-templates/listMembers.json rename to lexicon-templates/spaces/listMembers.json diff --git a/spaces-lexicon-templates/listRecords.json b/lexicon-templates/spaces/listRecords.json similarity index 100% rename from spaces-lexicon-templates/listRecords.json rename to lexicon-templates/spaces/listRecords.json diff --git a/spaces-lexicon-templates/listSpaces.json b/lexicon-templates/spaces/listSpaces.json similarity index 100% rename from spaces-lexicon-templates/listSpaces.json rename to lexicon-templates/spaces/listSpaces.json diff --git a/spaces-lexicon-templates/putRecord.json b/lexicon-templates/spaces/putRecord.json similarity index 100% rename from spaces-lexicon-templates/putRecord.json rename to lexicon-templates/spaces/putRecord.json diff --git a/spaces-lexicon-templates/removeMember.json b/lexicon-templates/spaces/removeMember.json similarity index 100% rename from spaces-lexicon-templates/removeMember.json rename to lexicon-templates/spaces/removeMember.json diff --git a/spaces-lexicon-templates/uploadBlob.json b/lexicon-templates/spaces/uploadBlob.json similarity index 100% rename from spaces-lexicon-templates/uploadBlob.json rename to lexicon-templates/spaces/uploadBlob.json diff --git a/lexicons/README.md b/lexicons/README.md index 45e1224..8534000 100644 --- a/lexicons/README.md +++ b/lexicons/README.md @@ -9,8 +9,10 @@ Place your own hand-authored lexicon JSON files here. Generated from the `pull.sources` list in `lex.config.js`. - **`lexicons-generated/`** — Auto-generated by `pnpm generate` from `app/config.ts`. Don't edit by hand. -- **`spaces-lexicon-templates/`** — Shipped library templates for permissioned spaces; - the generator instantiates them under `.space.*` into `lexicons-generated/`. +- **`lexicon-templates/{spaces,community,realtime}/`** — Shipped library templates; + the generator instantiates them under `.{space,community,realtime}.*` into + `lexicons-generated/`. `spaces/ext/` also emits `.spaceExt.*` (contrail + extras — invites, whoami — outside the permissioned-data spec). All three (`lexicons/`, `lexicons-pulled/`, `lexicons-generated/`) are picked up by `lex-cli generate` (see `lex.config.js` `files` glob). diff --git a/package.json b/package.json index 0b5edd9..9a5d155 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,7 @@ "sideEffects": false, "files": [ "dist", - "spaces-lexicon-templates/**/*.json", - "community-lexicon-templates/**/*.json", - "realtime-lexicon-templates/**/*.json" + "lexicon-templates/**/*.json" ], "exports": { ".": { @@ -39,9 +37,7 @@ "types": "./dist/sync/index.d.ts", "import": "./dist/sync/index.js" }, - "./spaces-lexicon-templates/*.json": "./spaces-lexicon-templates/*.json", - "./community-lexicon-templates/*.json": "./community-lexicon-templates/*.json", - "./realtime-lexicon-templates/*.json": "./realtime-lexicon-templates/*.json" + "./lexicon-templates/*.json": "./lexicon-templates/*.json" }, "repository": { "type": "git", diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index 4fef67a..a14c880 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -87,7 +87,7 @@ export function registerSpacesRoutes( /** Space endpoints are emitted per-deployment under the configured namespace; * the deployment owns and publishes its own lexicons. The library ships - * templates at `spaces-lexicon-templates/*` that the generator instantiates + * templates at `lexicon-templates/spaces/*` that the generator instantiates * under `.space.*` (spec-aligned) and `.spaceExt.*` (contrail * extras — invites, whoami — that the permissioned-data spec doesn't cover). */ const SPACE = `${config.namespace}.space`; diff --git a/src/generate.ts b/src/generate.ts index 2b43b2f..63e55d1 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -59,12 +59,15 @@ function fieldToParam(field: string): string { return field.replace(/\.(\w)/g, (_, c) => c.toUpperCase()); } -/** Locate the shipped space-template directory. Works in contrail's own repo - * and in downstream projects that depend on @atmo-dev/contrail. */ -function findSpaceTemplatesDir(rootDir: string): string | null { +/** Locate a shipped lexicon-template subdir (`spaces`/`community`/`realtime`). + * Works in contrail's own repo and in downstream projects that depend on + * @atmo-dev/contrail. All three module templates live under one + * `lexicon-templates/` parent; each subdir is instantiated by the generator + * into its own `.*` namespace. */ +function findTemplatesDir(rootDir: string, module: "spaces" | "community" | "realtime"): string | null { const candidates = [ - join(rootDir, "spaces-lexicon-templates"), - join(rootDir, "node_modules/@atmo-dev/contrail/spaces-lexicon-templates"), + join(rootDir, "lexicon-templates", module), + join(rootDir, "node_modules/@atmo-dev/contrail/lexicon-templates", module), ]; for (const p of candidates) { if (existsSync(p)) return p; @@ -72,27 +75,9 @@ function findSpaceTemplatesDir(rootDir: string): string | null { return null; } -function findCommunityTemplatesDir(rootDir: string): string | null { - const candidates = [ - join(rootDir, "community-lexicon-templates"), - join(rootDir, "node_modules/@atmo-dev/contrail/community-lexicon-templates"), - ]; - for (const p of candidates) { - if (existsSync(p)) return p; - } - return null; -} - -function findRealtimeTemplatesDir(rootDir: string): string | null { - const candidates = [ - join(rootDir, "realtime-lexicon-templates"), - join(rootDir, "node_modules/@atmo-dev/contrail/realtime-lexicon-templates"), - ]; - for (const p of candidates) { - if (existsSync(p)) return p; - } - return null; -} +const findSpaceTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "spaces"); +const findCommunityTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "community"); +const findRealtimeTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "realtime"); /** Yield all JSON files under a directory (recursive). */ function* walkJson(dir: string): Generator { -- 2.51.2 From 6c965fd0094ffd1b9a925f9b7dc8a26808beaf09 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:11:22 +0200 Subject: [PATCH 11/18] simplify endpoints --- .../atmo/chat/community/invite/create.json | 93 ----- .../atmo/chat/community/invite/list.json | 52 --- .../atmo/chat/community/invite/revoke.json | 46 --- .../atmo/chat/community/space/whoami.json | 42 --- .../chat/{spaceExt => }/invite/create.json | 25 +- .../tools/atmo/chat/invite/defs.json | 73 ++++ .../atmo/chat/{spaceExt => }/invite/list.json | 6 +- .../chat/{community => }/invite/redeem.json | 24 +- .../chat/{spaceExt => }/invite/revoke.json | 8 +- .../tools/atmo/chat/permissionSet.json | 13 +- .../atmo/chat/spaceExt/invite/redeem.json | 44 --- .../tools/atmo/chat/spaceExt/whoami.json | 12 +- .../src/lib/atproto/generated-methods.ts | 13 +- .../src/lib/rooms/server.ts | 10 +- .../routes/c/[communityDid]/+layout.server.ts | 2 +- .../community/invite/create.json | 45 --- lexicon-templates/community/invite/list.json | 35 -- .../community/invite/redeem.json | 37 -- .../community/invite/revoke.json | 34 -- lexicon-templates/community/space/whoami.json | 28 -- lexicon-templates/invite/create.json | 41 +++ lexicon-templates/invite/defs.json | 24 ++ .../{spaces/ext => }/invite/list.json | 6 +- lexicon-templates/invite/redeem.json | 37 ++ .../{spaces/ext => }/invite/revoke.json | 8 +- .../spaces/ext/invite/create.json | 39 --- .../spaces/ext/invite/redeem.json | 33 -- lexicon-templates/spaces/ext/whoami.json | 7 +- src/core/community/router.ts | 174 +--------- src/core/invite/index.ts | 2 + src/core/invite/router.ts | 317 ++++++++++++++++++ src/core/router/index.ts | 16 +- src/core/spaces/router.ts | 130 ++----- src/generate.ts | 51 ++- tests/community-delegation.test.ts | 2 +- tests/community-e2e.test.ts | 6 +- tests/community-mint.test.ts | 2 +- tests/invite-unified.test.ts | 213 ++++++++++++ tests/spaces-invites.test.ts | 44 +-- 39 files changed, 902 insertions(+), 892 deletions(-) delete mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json delete mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json delete mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json delete mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{spaceExt => }/invite/create.json (59%) create mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{spaceExt => }/invite/list.json (80%) rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{community => }/invite/redeem.json (58%) rename examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/{spaceExt => }/invite/revoke.json (69%) delete mode 100644 examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json delete mode 100644 lexicon-templates/community/invite/create.json delete mode 100644 lexicon-templates/community/invite/list.json delete mode 100644 lexicon-templates/community/invite/redeem.json delete mode 100644 lexicon-templates/community/invite/revoke.json delete mode 100644 lexicon-templates/community/space/whoami.json create mode 100644 lexicon-templates/invite/create.json create mode 100644 lexicon-templates/invite/defs.json rename lexicon-templates/{spaces/ext => }/invite/list.json (74%) create mode 100644 lexicon-templates/invite/redeem.json rename lexicon-templates/{spaces/ext => }/invite/revoke.json (59%) delete mode 100644 lexicon-templates/spaces/ext/invite/create.json delete mode 100644 lexicon-templates/spaces/ext/invite/redeem.json create mode 100644 src/core/invite/router.ts create mode 100644 tests/invite-unified.test.ts diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json deleted file mode 100644 index 9babcc7..0000000 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/create.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.chat.community.invite.create", - "defs": { - "main": { - "type": "procedure", - "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "spaceUri", - "accessLevel" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "accessLevel": { - "type": "ref", - "ref": "tools.atmo.chat.community.defs#accessLevel" - }, - "expiresAt": { - "type": "integer", - "description": "Unix ms. Omit for no expiry." - }, - "maxUses": { - "type": "integer", - "minimum": 1, - "description": "Omit for unlimited redemptions." - }, - "note": { - "type": "string", - "maxLength": 500 - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "token", - "tokenHash", - "spaceUri", - "accessLevel", - "createdAt" - ], - "properties": { - "token": { - "type": "string", - "description": "Raw token. Shown once — cannot be retrieved later." - }, - "tokenHash": { - "type": "string" - }, - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "accessLevel": { - "type": "ref", - "ref": "tools.atmo.chat.community.defs#accessLevel" - }, - "expiresAt": { - "type": "integer" - }, - "maxUses": { - "type": "integer" - }, - "createdAt": { - "type": "integer" - } - } - } - }, - "errors": [ - { - "name": "InvalidRequest" - }, - { - "name": "Forbidden" - }, - { - "name": "NotFound" - } - ] - } - } -} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json deleted file mode 100644 index 55ef50a..0000000 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/list.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.chat.community.invite.list", - "defs": { - "main": { - "type": "query", - "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", - "parameters": { - "type": "params", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "includeRevoked": { - "type": "boolean", - "default": false - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "invites" - ], - "properties": { - "invites": { - "type": "array", - "items": { - "type": "ref", - "ref": "tools.atmo.chat.community.defs#inviteView" - } - } - } - } - }, - "errors": [ - { - "name": "Forbidden" - }, - { - "name": "NotFound" - } - ] - } - } -} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json deleted file mode 100644 index 30f2cf0..0000000 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/revoke.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.chat.community.invite.revoke", - "defs": { - "main": { - "type": "procedure", - "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "tokenHash" - ], - "properties": { - "tokenHash": { - "type": "string" - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "ok" - ], - "properties": { - "ok": { - "type": "boolean" - } - } - } - }, - "errors": [ - { - "name": "NotFound" - }, - { - "name": "Forbidden" - } - ] - } - } -} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json deleted file mode 100644 index 457244d..0000000 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/whoami.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.chat.community.space.whoami", - "defs": { - "main": { - "type": "query", - "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", - "parameters": { - "type": "params", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - }, - "accessLevel": { - "type": "ref", - "ref": "tools.atmo.chat.community.defs#accessLevel", - "description": "Null when caller has no resolvable access." - } - } - } - } - } - } -} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/create.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/create.json similarity index 59% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/create.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/create.json index f6ed9fe..2d9b235 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/create.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/create.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "tools.atmo.chat.spaceExt.invite.create", + "id": "tools.atmo.chat.invite.create", "defs": { "main": { "type": "procedure", - "description": "Create an invite for a space. Caller must be the space owner. Returns the raw token once; only the hash is stored.", + "description": "Create an invite for a space. The service dispatches on space ownership: user-owned spaces take `kind` (default `join`); community-owned spaces take `accessLevel`. Exactly one of `kind` / `accessLevel` must be set. Returns the raw token once; only the hash is stored.", "input": { "encoding": "application/json", "schema": { @@ -24,8 +24,17 @@ "read", "read-join" ], - "default": "join", - "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." + "description": "For user-owned spaces. join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." + }, + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "For community-owned spaces. The access level granted on redemption — the creator's own level caps what they can grant." }, "expiresAt": { "type": "integer", @@ -33,8 +42,7 @@ }, "maxUses": { "type": "integer", - "minimum": 1, - "description": "Caps join redemptions only — read-token reads are unlimited. Omit for unlimited joins." + "minimum": 1 }, "note": { "type": "string", @@ -58,7 +66,7 @@ }, "invite": { "type": "ref", - "ref": "tools.atmo.chat.space.defs#inviteView" + "ref": "tools.atmo.chat.invite.defs#inviteView" } } } @@ -69,6 +77,9 @@ }, { "name": "Forbidden" + }, + { + "name": "InvalidRequest" } ] } diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json new file mode 100644 index 0000000..b631730 --- /dev/null +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json @@ -0,0 +1,73 @@ +{ + "lexicon": 1, + "id": "tools.atmo.chat.invite.defs", + "defs": { + "inviteView": { + "type": "object", + "description": "An invite row as exposed to clients. Either `kind` (user-owned space) or `accessLevel` (community-owned space) is set, never both.", + "required": [ + "tokenHash", + "spaceUri", + "createdBy", + "createdAt", + "usedCount" + ], + "properties": { + "tokenHash": { + "type": "string", + "description": "Stable identifier for list/revoke operations." + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "kind": { + "type": "string", + "knownValues": [ + "join", + "read", + "read-join" + ], + "description": "Set for user-owned spaces. Absent for community-owned." + }, + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "Set for community-owned spaces. Absent for user-owned." + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer", + "description": "Unix ms." + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms. Omitted for no expiry." + }, + "maxUses": { + "type": "integer", + "minimum": 1 + }, + "usedCount": { + "type": "integer" + }, + "revokedAt": { + "type": "integer", + "description": "Unix ms. Omitted if not revoked." + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + } + } +} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/list.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/list.json similarity index 80% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/list.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/list.json index d5191da..21a702e 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/list.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/list.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "tools.atmo.chat.spaceExt.invite.list", + "id": "tools.atmo.chat.invite.list", "defs": { "main": { "type": "query", - "description": "List invites for a space. Owner only.", + "description": "List invites for a space. User-owned spaces: owner-only. Community-owned spaces: manager+.", "parameters": { "type": "params", "required": [ @@ -33,7 +33,7 @@ "type": "array", "items": { "type": "ref", - "ref": "tools.atmo.chat.space.defs#inviteView" + "ref": "tools.atmo.chat.invite.defs#inviteView" } } } diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json similarity index 58% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json index bc83ece..86c2148 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/invite/redeem.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "tools.atmo.chat.community.invite.redeem", + "id": "tools.atmo.chat.invite.redeem", "defs": { "main": { "type": "procedure", - "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", + "description": "Redeem an invite token. User-owned spaces: caller becomes a member. Community-owned spaces: caller is granted the invite's access level.", "input": { "encoding": "application/json", "schema": { @@ -24,32 +24,32 @@ "schema": { "type": "object", "required": [ - "spaceUri", - "accessLevel", - "communityDid" + "spaceUri" ], "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, + "kind": { + "type": "string", + "description": "Set for user-owned spaces — echoes the invite kind consumed." + }, "accessLevel": { - "type": "ref", - "ref": "tools.atmo.chat.community.defs#accessLevel" + "type": "string", + "description": "Set for community-owned spaces — the level granted." }, "communityDid": { "type": "string", - "format": "did" + "format": "did", + "description": "Set for community-owned spaces." } } } }, "errors": [ { - "name": "InvalidRequest" - }, - { - "name": "InvalidToken" + "name": "InvalidInvite" }, { "name": "NotFound" diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/revoke.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json similarity index 69% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/revoke.json rename to examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json index 49d5d2e..4686cb9 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/revoke.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json @@ -1,22 +1,22 @@ { "lexicon": 1, - "id": "tools.atmo.chat.spaceExt.invite.revoke", + "id": "tools.atmo.chat.invite.revoke", "defs": { "main": { "type": "procedure", - "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", + "description": "Revoke an invite by tokenHash. User-owned spaces: owner-only. Community-owned spaces: invite creator OR manager+ on the target space.", "input": { "encoding": "application/json", "schema": { "type": "object", "required": [ - "spaceUri", "tokenHash" ], "properties": { "spaceUri": { "type": "string", - "format": "at-uri" + "format": "at-uri", + "description": "Optional — ownership is inferred from the invite row; required for user-owned spaces for a sanity check." }, "tokenHash": { "type": "string" diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json index 99c71cf..89a6a5a 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json @@ -19,10 +19,6 @@ "tools.atmo.chat.community.delete", "tools.atmo.chat.community.deleteRecord", "tools.atmo.chat.community.getHealth", - "tools.atmo.chat.community.invite.create", - "tools.atmo.chat.community.invite.list", - "tools.atmo.chat.community.invite.redeem", - "tools.atmo.chat.community.invite.revoke", "tools.atmo.chat.community.list", "tools.atmo.chat.community.mint", "tools.atmo.chat.community.putRecord", @@ -36,10 +32,13 @@ "tools.atmo.chat.community.space.resync", "tools.atmo.chat.community.space.revoke", "tools.atmo.chat.community.space.setAccessLevel", - "tools.atmo.chat.community.space.whoami", "tools.atmo.chat.getCursor", "tools.atmo.chat.getOverview", "tools.atmo.chat.getProfile", + "tools.atmo.chat.invite.create", + "tools.atmo.chat.invite.list", + "tools.atmo.chat.invite.redeem", + "tools.atmo.chat.invite.revoke", "tools.atmo.chat.message.getRecord", "tools.atmo.chat.message.listRecords", "tools.atmo.chat.message.watchRecords", @@ -63,10 +62,6 @@ "tools.atmo.chat.space.putRecord", "tools.atmo.chat.space.removeMember", "tools.atmo.chat.space.uploadBlob", - "tools.atmo.chat.spaceExt.invite.create", - "tools.atmo.chat.spaceExt.invite.list", - "tools.atmo.chat.spaceExt.invite.redeem", - "tools.atmo.chat.spaceExt.invite.revoke", "tools.atmo.chat.spaceExt.whoami" ] } diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json deleted file mode 100644 index 9d9e552..0000000 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/invite/redeem.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.chat.spaceExt.invite.redeem", - "defs": { - "main": { - "type": "procedure", - "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" - } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "spaceUri" - ], - "properties": { - "spaceUri": { - "type": "string", - "format": "at-uri" - } - } - } - }, - "errors": [ - { - "name": "InvalidInvite" - } - ] - } - } -} diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json index 83d5849..75b5e2e 100644 --- a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json +++ b/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", + "description": "Caller's relationship to a space. For user-owned spaces membership is binary (`isMember`). For community-owned spaces `accessLevel` is also returned, resolved through the community's access-level ladder (including delegated grants).", "parameters": { "type": "params", "required": [ @@ -31,6 +31,16 @@ }, "isMember": { "type": "boolean" + }, + "accessLevel": { + "type": "string", + "knownValues": [ + "member", + "manager", + "admin", + "owner" + ], + "description": "Only set for community-owned spaces. Null when the caller has no resolvable access." } } } diff --git a/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts b/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts index 0cfd48f..0458f77 100644 --- a/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts +++ b/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts @@ -9,10 +9,6 @@ export const xrpcMethods = [ "tools.atmo.chat.community.delete", "tools.atmo.chat.community.deleteRecord", "tools.atmo.chat.community.getHealth", - "tools.atmo.chat.community.invite.create", - "tools.atmo.chat.community.invite.list", - "tools.atmo.chat.community.invite.redeem", - "tools.atmo.chat.community.invite.revoke", "tools.atmo.chat.community.list", "tools.atmo.chat.community.mint", "tools.atmo.chat.community.putRecord", @@ -26,10 +22,13 @@ export const xrpcMethods = [ "tools.atmo.chat.community.space.resync", "tools.atmo.chat.community.space.revoke", "tools.atmo.chat.community.space.setAccessLevel", - "tools.atmo.chat.community.space.whoami", "tools.atmo.chat.getCursor", "tools.atmo.chat.getOverview", "tools.atmo.chat.getProfile", + "tools.atmo.chat.invite.create", + "tools.atmo.chat.invite.list", + "tools.atmo.chat.invite.redeem", + "tools.atmo.chat.invite.revoke", "tools.atmo.chat.message.getRecord", "tools.atmo.chat.message.listRecords", "tools.atmo.chat.message.watchRecords", @@ -53,9 +52,5 @@ export const xrpcMethods = [ "tools.atmo.chat.space.putRecord", "tools.atmo.chat.space.removeMember", "tools.atmo.chat.space.uploadBlob", - "tools.atmo.chat.spaceExt.invite.create", - "tools.atmo.chat.spaceExt.invite.list", - "tools.atmo.chat.spaceExt.invite.redeem", - "tools.atmo.chat.spaceExt.invite.revoke", "tools.atmo.chat.spaceExt.whoami" ] as const; diff --git a/examples/sveltekit-group-chat/src/lib/rooms/server.ts b/examples/sveltekit-group-chat/src/lib/rooms/server.ts index c8e0e99..0aa444a 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/server.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/server.ts @@ -213,7 +213,7 @@ export async function whoami( ctx: AuthedCallContext, query: { spaceUri: string } ): Promise<{ spaceUri: string; accessLevel: string | null }> { - return callContrail(ctx, 'tools.atmo.chat.community.space.whoami', { query }); + return callContrail(ctx, 'tools.atmo.chat.spaceExt.whoami', { query }); } // --- realtime -------------------------------------------------------------- @@ -258,7 +258,7 @@ export async function createCommunityInvite( maxUses: number | null; createdAt: number; }> { - return callContrail(ctx, 'tools.atmo.chat.community.invite.create', { body: input }); + return callContrail(ctx, 'tools.atmo.chat.invite.create', { body: input }); } export async function listCommunityInvites( @@ -267,21 +267,21 @@ export async function listCommunityInvites( ): Promise<{ invites: InviteView[] }> { const q: Record = { spaceUri: query.spaceUri }; if (query.includeRevoked) q.includeRevoked = 'true'; - return callContrail(ctx, 'tools.atmo.chat.community.invite.list', { query: q }); + return callContrail(ctx, 'tools.atmo.chat.invite.list', { query: q }); } export async function revokeCommunityInvite( ctx: AuthedCallContext, input: { tokenHash: string } ): Promise<{ ok: true }> { - return callContrail(ctx, 'tools.atmo.chat.community.invite.revoke', { body: input }); + return callContrail(ctx, 'tools.atmo.chat.invite.revoke', { body: input }); } export async function redeemCommunityInvite( ctx: AuthedCallContext, input: { token: string } ): Promise<{ spaceUri: string; accessLevel: string; communityDid: string }> { - return callContrail(ctx, 'tools.atmo.chat.community.invite.redeem', { body: input }); + return callContrail(ctx, 'tools.atmo.chat.invite.redeem', { body: input }); } // --- blobs ----------------------------------------------------------------- diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts index d3f554a..ff8f27b 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts @@ -118,7 +118,7 @@ export const load: LayoutServerLoad = async ({ locals, params, platform }) => { const adminUri = buildAdminUri(communityDid); const d = await authedFetch<{ accessLevel: string | null }>( ctx, - 'tools.atmo.chat.community.space.whoami', + 'tools.atmo.chat.spaceExt.whoami', { query: { spaceUri: adminUri } } ); isAdmin = d.accessLevel === 'admin' || d.accessLevel === 'owner'; diff --git a/lexicon-templates/community/invite/create.json b/lexicon-templates/community/invite/create.json deleted file mode 100644 index de9a4d4..0000000 --- a/lexicon-templates/community/invite/create.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.community.invite.create", - "defs": { - "main": { - "type": "procedure", - "description": "Create an invite for a community-owned space. Caller must have manager+ on the target space and cannot grant a higher level than their own. Returns the raw token once; only its SHA-256 is stored.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["spaceUri", "accessLevel"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, - "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, - "expiresAt": { "type": "integer", "description": "Unix ms. Omit for no expiry." }, - "maxUses": { "type": "integer", "minimum": 1, "description": "Omit for unlimited redemptions." }, - "note": { "type": "string", "maxLength": 500 } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["token", "tokenHash", "spaceUri", "accessLevel", "createdAt"], - "properties": { - "token": { "type": "string", "description": "Raw token. Shown once — cannot be retrieved later." }, - "tokenHash": { "type": "string" }, - "spaceUri": { "type": "string", "format": "at-uri" }, - "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, - "expiresAt": { "type": "integer" }, - "maxUses": { "type": "integer" }, - "createdAt": { "type": "integer" } - } - } - }, - "errors": [ - { "name": "InvalidRequest" }, - { "name": "Forbidden" }, - { "name": "NotFound" } - ] - } - } -} diff --git a/lexicon-templates/community/invite/list.json b/lexicon-templates/community/invite/list.json deleted file mode 100644 index 4ee794f..0000000 --- a/lexicon-templates/community/invite/list.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.community.invite.list", - "defs": { - "main": { - "type": "query", - "description": "List invites for a community-owned space. Caller must have manager+ on the space. Raw tokens are never returned — tokenHash is the stable id.", - "parameters": { - "type": "params", - "required": ["spaceUri"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, - "includeRevoked": { "type": "boolean", "default": false } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["invites"], - "properties": { - "invites": { - "type": "array", - "items": { "type": "ref", "ref": "tools.atmo.community.defs#inviteView" } - } - } - } - }, - "errors": [ - { "name": "Forbidden" }, - { "name": "NotFound" } - ] - } - } -} diff --git a/lexicon-templates/community/invite/redeem.json b/lexicon-templates/community/invite/redeem.json deleted file mode 100644 index 5935aa0..0000000 --- a/lexicon-templates/community/invite/redeem.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.community.invite.redeem", - "defs": { - "main": { - "type": "procedure", - "description": "Redeem a community invite. The caller's DID (from service-auth) is granted the access level encoded in the invite. Atomic — a token is consumed only if the invite is currently usable.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["token"], - "properties": { - "token": { "type": "string" } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["spaceUri", "accessLevel", "communityDid"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, - "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel" }, - "communityDid": { "type": "string", "format": "did" } - } - } - }, - "errors": [ - { "name": "InvalidRequest" }, - { "name": "InvalidToken" }, - { "name": "NotFound" } - ] - } - } -} diff --git a/lexicon-templates/community/invite/revoke.json b/lexicon-templates/community/invite/revoke.json deleted file mode 100644 index 05b3054..0000000 --- a/lexicon-templates/community/invite/revoke.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.community.invite.revoke", - "defs": { - "main": { - "type": "procedure", - "description": "Revoke an invite. Caller must be the invite's creator or have manager+ on the target space.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["tokenHash"], - "properties": { - "tokenHash": { "type": "string" } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["ok"], - "properties": { - "ok": { "type": "boolean" } - } - } - }, - "errors": [ - { "name": "NotFound" }, - { "name": "Forbidden" } - ] - } - } -} diff --git a/lexicon-templates/community/space/whoami.json b/lexicon-templates/community/space/whoami.json deleted file mode 100644 index fd0a6fa..0000000 --- a/lexicon-templates/community/space/whoami.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.community.space.whoami", - "defs": { - "main": { - "type": "query", - "description": "Report the JWT issuer's effective access level in a space (resolving delegated memberships).", - "parameters": { - "type": "params", - "required": ["spaceUri"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["spaceUri"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, - "accessLevel": { "type": "ref", "ref": "tools.atmo.community.defs#accessLevel", "description": "Null when caller has no resolvable access." } - } - } - } - } - } -} diff --git a/lexicon-templates/invite/create.json b/lexicon-templates/invite/create.json new file mode 100644 index 0000000..81910c0 --- /dev/null +++ b/lexicon-templates/invite/create.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "tools.atmo.invite.create", + "defs": { + "main": { + "type": "procedure", + "description": "Create an invite for a space. The service dispatches on space ownership: user-owned spaces take `kind` (default `join`); community-owned spaces take `accessLevel`. Exactly one of `kind` / `accessLevel` must be set. Returns the raw token once; only the hash is stored.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "kind": { "type": "string", "knownValues": ["join", "read", "read-join"], "description": "For user-owned spaces. join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." }, + "accessLevel": { "type": "string", "knownValues": ["member", "manager", "admin", "owner"], "description": "For community-owned spaces. The access level granted on redemption — the creator's own level caps what they can grant." }, + "expiresAt": { "type": "integer", "description": "Unix ms timestamp. Omit for no expiry." }, + "maxUses": { "type": "integer", "minimum": 1 }, + "note": { "type": "string", "maxLength": 500 } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["token", "invite"], + "properties": { + "token": { "type": "string", "description": "Raw token. Shown once — cannot be retrieved later." }, + "invite": { "type": "ref", "ref": "tools.atmo.invite.defs#inviteView" } + } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" }, + { "name": "InvalidRequest" } + ] + } + } +} diff --git a/lexicon-templates/invite/defs.json b/lexicon-templates/invite/defs.json new file mode 100644 index 0000000..909e5cd --- /dev/null +++ b/lexicon-templates/invite/defs.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "tools.atmo.invite.defs", + "defs": { + "inviteView": { + "type": "object", + "description": "An invite row as exposed to clients. Either `kind` (user-owned space) or `accessLevel` (community-owned space) is set, never both.", + "required": ["tokenHash", "spaceUri", "createdBy", "createdAt", "usedCount"], + "properties": { + "tokenHash": { "type": "string", "description": "Stable identifier for list/revoke operations." }, + "spaceUri": { "type": "string", "format": "at-uri" }, + "kind": { "type": "string", "knownValues": ["join", "read", "read-join"], "description": "Set for user-owned spaces. Absent for community-owned." }, + "accessLevel": { "type": "string", "knownValues": ["member", "manager", "admin", "owner"], "description": "Set for community-owned spaces. Absent for user-owned." }, + "createdBy": { "type": "string", "format": "did" }, + "createdAt": { "type": "integer", "description": "Unix ms." }, + "expiresAt": { "type": "integer", "description": "Unix ms. Omitted for no expiry." }, + "maxUses": { "type": "integer", "minimum": 1 }, + "usedCount": { "type": "integer" }, + "revokedAt": { "type": "integer", "description": "Unix ms. Omitted if not revoked." }, + "note": { "type": "string", "maxLength": 500 } + } + } + } +} diff --git a/lexicon-templates/spaces/ext/invite/list.json b/lexicon-templates/invite/list.json similarity index 74% rename from lexicon-templates/spaces/ext/invite/list.json rename to lexicon-templates/invite/list.json index ed316f9..a75afd7 100644 --- a/lexicon-templates/spaces/ext/invite/list.json +++ b/lexicon-templates/invite/list.json @@ -1,10 +1,10 @@ { "lexicon": 1, - "id": "tools.atmo.spaceExt.invite.list", + "id": "tools.atmo.invite.list", "defs": { "main": { "type": "query", - "description": "List invites for a space. Owner only.", + "description": "List invites for a space. User-owned spaces: owner-only. Community-owned spaces: manager+.", "parameters": { "type": "params", "required": ["spaceUri"], @@ -21,7 +21,7 @@ "properties": { "invites": { "type": "array", - "items": { "type": "ref", "ref": "tools.atmo.space.defs#inviteView" } + "items": { "type": "ref", "ref": "tools.atmo.invite.defs#inviteView" } } } } diff --git a/lexicon-templates/invite/redeem.json b/lexicon-templates/invite/redeem.json new file mode 100644 index 0000000..f013492 --- /dev/null +++ b/lexicon-templates/invite/redeem.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "tools.atmo.invite.redeem", + "defs": { + "main": { + "type": "procedure", + "description": "Redeem an invite token. User-owned spaces: caller becomes a member. Community-owned spaces: caller is granted the invite's access level.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["token"], + "properties": { + "token": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "kind": { "type": "string", "description": "Set for user-owned spaces — echoes the invite kind consumed." }, + "accessLevel": { "type": "string", "description": "Set for community-owned spaces — the level granted." }, + "communityDid": { "type": "string", "format": "did", "description": "Set for community-owned spaces." } + } + } + }, + "errors": [ + { "name": "InvalidInvite" }, + { "name": "NotFound" } + ] + } + } +} diff --git a/lexicon-templates/spaces/ext/invite/revoke.json b/lexicon-templates/invite/revoke.json similarity index 59% rename from lexicon-templates/spaces/ext/invite/revoke.json rename to lexicon-templates/invite/revoke.json index 13ecf62..aa534c1 100644 --- a/lexicon-templates/spaces/ext/invite/revoke.json +++ b/lexicon-templates/invite/revoke.json @@ -1,17 +1,17 @@ { "lexicon": 1, - "id": "tools.atmo.spaceExt.invite.revoke", + "id": "tools.atmo.invite.revoke", "defs": { "main": { "type": "procedure", - "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", + "description": "Revoke an invite by tokenHash. User-owned spaces: owner-only. Community-owned spaces: invite creator OR manager+ on the target space.", "input": { "encoding": "application/json", "schema": { "type": "object", - "required": ["spaceUri", "tokenHash"], + "required": ["tokenHash"], "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, + "spaceUri": { "type": "string", "format": "at-uri", "description": "Optional — ownership is inferred from the invite row; required for user-owned spaces for a sanity check." }, "tokenHash": { "type": "string" } } } diff --git a/lexicon-templates/spaces/ext/invite/create.json b/lexicon-templates/spaces/ext/invite/create.json deleted file mode 100644 index 4450aed..0000000 --- a/lexicon-templates/spaces/ext/invite/create.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.spaceExt.invite.create", - "defs": { - "main": { - "type": "procedure", - "description": "Create an invite for a space. Caller must be the space owner. Returns the raw token once; only the hash is stored.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["spaceUri"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" }, - "kind": { "type": "string", "knownValues": ["join", "read", "read-join"], "default": "join", "description": "join: redeem to become a member. read: bearer-only read access, no membership. read-join: anonymous read + signed-in redeem to join." }, - "expiresAt": { "type": "integer", "description": "Unix ms timestamp. Omit for no expiry." }, - "maxUses": { "type": "integer", "minimum": 1, "description": "Caps join redemptions only — read-token reads are unlimited. Omit for unlimited joins." }, - "note": { "type": "string", "maxLength": 500 } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["token", "invite"], - "properties": { - "token": { "type": "string", "description": "Raw token. Shown once — cannot be retrieved later." }, - "invite": { "type": "ref", "ref": "tools.atmo.space.defs#inviteView" } - } - } - }, - "errors": [ - { "name": "NotFound" }, - { "name": "Forbidden" } - ] - } - } -} diff --git a/lexicon-templates/spaces/ext/invite/redeem.json b/lexicon-templates/spaces/ext/invite/redeem.json deleted file mode 100644 index d0372c2..0000000 --- a/lexicon-templates/spaces/ext/invite/redeem.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "lexicon": 1, - "id": "tools.atmo.spaceExt.invite.redeem", - "defs": { - "main": { - "type": "procedure", - "description": "Redeem an invite token. The JWT issuer becomes a member of the space.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["token"], - "properties": { - "token": { "type": "string" } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["spaceUri"], - "properties": { - "spaceUri": { "type": "string", "format": "at-uri" } - } - } - }, - "errors": [ - { "name": "InvalidInvite" } - ] - } - } -} diff --git a/lexicon-templates/spaces/ext/whoami.json b/lexicon-templates/spaces/ext/whoami.json index 5736c86..0a61f21 100644 --- a/lexicon-templates/spaces/ext/whoami.json +++ b/lexicon-templates/spaces/ext/whoami.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Report the caller's relationship to a space: whether they are the owner and/or a member. Useful for clients to avoid a listMembers roundtrip.", + "description": "Caller's relationship to a space. For user-owned spaces membership is binary (`isMember`). For community-owned spaces `accessLevel` is also returned, resolved through the community's access-level ladder (including delegated grants).", "parameters": { "type": "params", "required": ["spaceUri"], @@ -18,8 +18,9 @@ "type": "object", "required": ["isOwner", "isMember"], "properties": { - "isOwner": { "type": "boolean" }, - "isMember": { "type": "boolean" } + "isOwner": { "type": "boolean" }, + "isMember": { "type": "boolean" }, + "accessLevel": { "type": "string", "knownValues": ["member", "manager", "admin", "owner"], "description": "Only set for community-owned spaces. Null when the caller has no resolvable access." } } } }, diff --git a/src/core/community/router.ts b/src/core/community/router.ts index b3dfcd2..99b1d76 100644 --- a/src/core/community/router.ts +++ b/src/core/community/router.ts @@ -25,8 +25,6 @@ import { RESERVED_KEYS, } from "./types"; import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; -import { hashInviteToken, mintInviteToken } from "../invite/token"; - export interface CommunityRoutesOptions { /** Override auth middleware for tests. */ authMiddleware?: MiddlewareHandler; @@ -254,15 +252,8 @@ export function registerCommunityRoutes( }); }); - app.get(`/xrpc/${NS}.space.whoami`, auth, async (c) => { - const sa = getAuth(c); - const spaceUri = c.req.query("spaceUri"); - if (!spaceUri) { - return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); - } - const level = await resolveEffectiveLevel(community, spaceUri, sa.issuer); - return c.json({ spaceUri, accessLevel: level }); - }); + // `whoami` lives under `.spaceExt.whoami` — unified across user-owned + // and community-owned spaces, returns `{ isOwner, isMember, accessLevel? }`. // ========================================================================== // Space lifecycle @@ -973,164 +964,9 @@ export function registerCommunityRoutes( return c.json({ ok: true }); }); - // ========================================================================== - // Invites — pre-signed grants for community-owned spaces. Admin/manager - // creates; any authenticated user with the raw token redeems once. - // ========================================================================== - - app.post(`/xrpc/${NS}.invite.create`, auth, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as - | { - spaceUri?: string; - accessLevel?: string; - expiresAt?: number; - maxUses?: number; - note?: string; - } - | null; - if (!body?.spaceUri || !body.accessLevel) { - return c.json( - { error: "InvalidRequest", message: "spaceUri and accessLevel required" }, - 400 - ); - } - if (!isAccessLevel(body.accessLevel)) { - return c.json({ error: "InvalidRequest", message: "invalid accessLevel" }, 400); - } - const space = await spaces.getSpace(body.spaceUri); - if (!space) return c.json({ error: "NotFound" }, 404); - const communityRow = await community.getCommunity(space.ownerDid); - if (!communityRow) { - return c.json({ error: "InvalidRequest", reason: "not-community-owned" }, 400); - } - - // Caller must have at least manager on the target space, and cannot create - // an invite that confers a higher level than their own. - const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); - if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { - return c.json({ error: "Forbidden", reason: "manager-required" }, 403); - } - if (rankOf(body.accessLevel) > rankOf(callerLevel)) { - return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); - } - - const { token, tokenHash } = await mintInviteToken(); - - const row = await community.createInvite({ - spaceUri: body.spaceUri, - tokenHash, - accessLevel: body.accessLevel, - createdBy: sa.issuer, - expiresAt: body.expiresAt ?? null, - maxUses: body.maxUses ?? null, - note: body.note ?? null, - }); - - // Raw token only returned once. tokenHash is the stable ID for list/revoke. - return c.json({ - token, - tokenHash, - spaceUri: row.spaceUri, - accessLevel: row.accessLevel, - expiresAt: row.expiresAt, - maxUses: row.maxUses, - createdAt: row.createdAt, - }); - }); - - app.get(`/xrpc/${NS}.invite.list`, auth, async (c) => { - const sa = getAuth(c); - const spaceUri = c.req.query("spaceUri"); - const includeRevoked = c.req.query("includeRevoked") === "true"; - if (!spaceUri) { - return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); - } - const space = await spaces.getSpace(spaceUri); - if (!space) return c.json({ error: "NotFound" }, 404); - - const callerLevel = await resolveEffectiveLevel(community, spaceUri, sa.issuer); - if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { - return c.json({ error: "Forbidden", reason: "manager-required" }, 403); - } - - const rows = await community.listInvites(spaceUri, { includeRevoked }); - return c.json({ - invites: rows.map((r) => ({ - tokenHash: r.tokenHash, - spaceUri: r.spaceUri, - accessLevel: r.accessLevel, - createdBy: r.createdBy, - createdAt: r.createdAt, - expiresAt: r.expiresAt, - maxUses: r.maxUses, - usedCount: r.usedCount, - revokedAt: r.revokedAt, - note: r.note, - })), - }); - }); - - app.post(`/xrpc/${NS}.invite.revoke`, auth, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as - | { tokenHash?: string } - | null; - if (!body?.tokenHash) { - return c.json({ error: "InvalidRequest", message: "tokenHash required" }, 400); - } - const invite = await community.getInvite(body.tokenHash); - if (!invite) return c.json({ error: "NotFound" }, 404); - - // Revoker: either the invite's creator, or manager+ on the target space. - let allowed = invite.createdBy === sa.issuer; - if (!allowed) { - const level = await resolveEffectiveLevel(community, invite.spaceUri, sa.issuer); - allowed = !!level && rankOf(level) >= rankOf("manager"); - } - if (!allowed) { - return c.json({ error: "Forbidden", reason: "creator-or-manager-required" }, 403); - } - await community.revokeInvite(body.tokenHash); - return c.json({ ok: true }); - }); - - app.post(`/xrpc/${NS}.invite.redeem`, auth, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as { token?: string } | null; - if (!body?.token) { - return c.json({ error: "InvalidRequest", message: "token required" }, 400); - } - const tokenHash = await hashInviteToken(body.token); - - // Atomic consume — returns null if the token is expired/revoked/exhausted. - const invite = await community.redeemInvite(tokenHash, Date.now()); - if (!invite) { - return c.json({ error: "InvalidToken", reason: "token-invalid-or-exhausted" }, 410); - } - - const space = await spaces.getSpace(invite.spaceUri); - if (!space) { - return c.json({ error: "NotFound", reason: "space-not-found" }, 404); - } - - // The token itself is the authorization: the creator (who had manager+) - // pre-signed "anyone with this token gets level X on this space". Grant - // directly, attributing to the creator so audit trails make sense. - await community.grant({ - spaceUri: invite.spaceUri, - subjectDid: sa.issuer, - accessLevel: invite.accessLevel, - grantedBy: invite.createdBy, - }); - await reconcile(community, spaces, invite.spaceUri, invite.createdBy); - - return c.json({ - spaceUri: invite.spaceUri, - accessLevel: invite.accessLevel, - communityDid: space.ownerDid, - }); - }); + // Invites live under `.invite.*` — see src/core/invite/router.ts. The + // unified surface dispatches on space ownership to choose between the + // ladder-granting path here and the binary-membership path in spaces. } // ============================================================================ diff --git a/src/core/invite/index.ts b/src/core/invite/index.ts index d79fc3b..8dae0df 100644 --- a/src/core/invite/index.ts +++ b/src/core/invite/index.ts @@ -1 +1,3 @@ export { generateInviteToken, hashInviteToken, mintInviteToken } from "./token"; +export { registerInviteRoutes } from "./router"; +export type { InviteRoutesOptions } from "./router"; diff --git a/src/core/invite/router.ts b/src/core/invite/router.ts new file mode 100644 index 0000000..0b9a4fc --- /dev/null +++ b/src/core/invite/router.ts @@ -0,0 +1,317 @@ +/** Unified invite surface: a single `.invite.*` family serving both + * user-owned and community-owned spaces. Dispatches on space ownership. + * + * - User-owned space → `kind` in create, `addMember` on redeem, owner-only. + * - Community-owned → `accessLevel` in create, `grant` on redeem, + * manager+ with "cannot grant higher than self". + * + * Storage stays separate (`spaces_invites` vs `community_invites` tables) — + * schemas differ enough that unifying them would be net-negative. The token + * primitive and HTTP dance are shared. */ + +import type { Context, Hono, MiddlewareHandler } from "hono"; +import type { ContrailConfig } from "../types"; +import type { ServiceAuth } from "../spaces/auth"; +import type { StorageAdapter } from "../spaces/types"; +import type { InviteKind, InviteRow } from "../spaces/types"; +import type { CommunityAdapter } from "../community/adapter"; +import type { CommunityInviteRow, AccessLevel } from "../community/types"; +import { isAccessLevel, rankOf } from "../community/types"; +import { hashInviteToken, mintInviteToken } from "./token"; +import { resolveEffectiveLevel } from "../community/acl"; +import { reconcile } from "../community/reconcile"; + +export interface InviteRoutesOptions { + authMiddleware: MiddlewareHandler; +} + +/** Shape returned to clients — `kind` (user-owned space) or `accessLevel` + * (community-owned space) is set, never both. */ +interface PublicInviteView { + tokenHash: string; + spaceUri: string; + kind?: InviteKind; + accessLevel?: AccessLevel; + createdBy: string; + createdAt: number; + expiresAt: number | null; + maxUses: number | null; + usedCount: number; + revokedAt: number | null; + note: string | null; +} + +function toSpacesView(row: InviteRow): PublicInviteView { + return { + tokenHash: row.tokenHash, + spaceUri: row.spaceUri, + kind: row.kind, + createdBy: row.createdBy, + createdAt: row.createdAt, + expiresAt: row.expiresAt, + maxUses: row.maxUses, + usedCount: row.usedCount, + revokedAt: row.revokedAt, + note: row.note, + }; +} + +function toCommunityView(row: CommunityInviteRow): PublicInviteView { + return { + tokenHash: row.tokenHash, + spaceUri: row.spaceUri, + accessLevel: row.accessLevel, + createdBy: row.createdBy, + createdAt: row.createdAt, + expiresAt: row.expiresAt, + maxUses: row.maxUses, + usedCount: row.usedCount, + revokedAt: row.revokedAt, + note: row.note, + }; +} + +export function registerInviteRoutes( + app: Hono, + config: ContrailConfig, + spaces: StorageAdapter, + community: CommunityAdapter | null, + options: InviteRoutesOptions +): void { + if (!config.spaces) return; + + const NS = `${config.namespace}.invite`; + const auth = options.authMiddleware; + + /** Resolve whether a space is community-owned. Returns null if the space + * doesn't exist. */ + const classifySpace = async (spaceUri: string) => { + const space = await spaces.getSpace(spaceUri); + if (!space) return null; + const isCommunity = community ? !!(await community.getCommunity(space.ownerDid)) : false; + return { space, isCommunity }; + }; + + app.post(`/xrpc/${NS}.create`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { + spaceUri?: string; + kind?: string; + accessLevel?: string; + expiresAt?: number; + maxUses?: number; + note?: string; + } + | null; + if (!body?.spaceUri) { + return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + } + if (body.kind && body.accessLevel) { + return c.json( + { error: "InvalidRequest", reason: "kind-or-accessLevel", message: "pass kind OR accessLevel, not both" }, + 400 + ); + } + + const classified = await classifySpace(body.spaceUri); + if (!classified) return c.json({ error: "NotFound" }, 404); + const { space, isCommunity } = classified; + + if (isCommunity) { + if (!community) return c.json({ error: "InvalidState" }, 500); + if (body.kind) { + return c.json( + { error: "InvalidRequest", reason: "kind-on-community-space", message: "community spaces take accessLevel, not kind" }, + 400 + ); + } + if (!body.accessLevel || !isAccessLevel(body.accessLevel)) { + return c.json({ error: "InvalidRequest", reason: "accessLevel-required" }, 400); + } + // Caller must have manager+ on the target space and cannot create an + // invite that confers a higher level than their own. + const callerLevel = await resolveEffectiveLevel(community, body.spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + if (rankOf(body.accessLevel) > rankOf(callerLevel)) { + return c.json({ error: "Forbidden", reason: "cannot-grant-higher-than-self" }, 403); + } + const { token, tokenHash } = await mintInviteToken(); + const row = await community.createInvite({ + spaceUri: body.spaceUri, + tokenHash, + accessLevel: body.accessLevel, + createdBy: sa.issuer, + expiresAt: body.expiresAt ?? null, + maxUses: body.maxUses ?? null, + note: body.note ?? null, + }); + return c.json({ token, invite: toCommunityView(row) }); + } + + // User-owned space. + if (body.accessLevel) { + return c.json( + { error: "InvalidRequest", reason: "accessLevel-on-user-space", message: "user-owned spaces take kind, not accessLevel" }, + 400 + ); + } + if (space.ownerDid !== sa.issuer) { + return c.json({ error: "Forbidden", reason: "not-owner" }, 403); + } + const kind = (body.kind ?? "join") as InviteKind; + if (kind !== "join" && kind !== "read" && kind !== "read-join") { + return c.json({ error: "InvalidRequest", message: "kind must be 'join', 'read', or 'read-join'" }, 400); + } + const { token, tokenHash } = await mintInviteToken(); + const invite = await spaces.createInvite({ + spaceUri: body.spaceUri, + tokenHash, + kind, + expiresAt: body.expiresAt ?? null, + maxUses: body.maxUses ?? null, + createdBy: sa.issuer, + note: body.note ?? null, + }); + return c.json({ token, invite: toSpacesView(invite) }); + }); + + app.get(`/xrpc/${NS}.list`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + if (!spaceUri) return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + const includeRevoked = c.req.query("includeRevoked") === "true"; + + const classified = await classifySpace(spaceUri); + if (!classified) return c.json({ error: "NotFound" }, 404); + const { space, isCommunity } = classified; + + if (isCommunity) { + const callerLevel = await resolveEffectiveLevel(community!, spaceUri, sa.issuer); + if (!callerLevel || rankOf(callerLevel) < rankOf("manager")) { + return c.json({ error: "Forbidden", reason: "manager-required" }, 403); + } + const rows = await community!.listInvites(spaceUri, { includeRevoked }); + return c.json({ invites: rows.map(toCommunityView) }); + } + + if (space.ownerDid !== sa.issuer) { + return c.json({ error: "Forbidden", reason: "not-owner" }, 403); + } + const rows = await spaces.listInvites(spaceUri, { includeRevoked }); + return c.json({ invites: rows.map(toSpacesView) }); + }); + + app.post(`/xrpc/${NS}.revoke`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { spaceUri?: string; tokenHash?: string } + | null; + if (!body?.tokenHash) { + return c.json({ error: "InvalidRequest", message: "tokenHash required" }, 400); + } + + // When the caller passes spaceUri we do an auth check up front so the + // response doesn't leak token existence. Community revokers may also be + // the invite creator (even without manager+), which is resolved after. + if (body.spaceUri) { + const classified = await classifySpace(body.spaceUri); + if (!classified) return c.json({ error: "NotFound" }, 404); + if (classified.isCommunity) { + const level = await resolveEffectiveLevel(community!, body.spaceUri, sa.issuer); + const managerOrHigher = !!level && rankOf(level) >= rankOf("manager"); + if (!managerOrHigher) { + const crow = await community!.getInvite(body.tokenHash); + if (!crow || crow.createdBy !== sa.issuer) { + return c.json({ error: "Forbidden", reason: "creator-or-manager-required" }, 403); + } + } + const ok = await community!.revokeInvite(body.tokenHash); + return c.json({ ok }); + } + if (classified.space.ownerDid !== sa.issuer) { + return c.json({ error: "Forbidden", reason: "not-owner" }, 403); + } + const ok = await spaces.revokeInvite(body.tokenHash); + return c.json({ ok }); + } + + // No spaceUri provided — infer from the invite row. + if (community) { + const crow = await community.getInvite(body.tokenHash); + if (crow) { + let allowed = crow.createdBy === sa.issuer; + if (!allowed) { + const level = await resolveEffectiveLevel(community, crow.spaceUri, sa.issuer); + allowed = !!level && rankOf(level) >= rankOf("manager"); + } + if (!allowed) { + return c.json({ error: "Forbidden", reason: "creator-or-manager-required" }, 403); + } + const ok = await community.revokeInvite(body.tokenHash); + return c.json({ ok }); + } + } + const srow = await spaces.getInvite(body.tokenHash); + if (!srow) return c.json({ error: "NotFound" }, 404); + const space = await spaces.getSpace(srow.spaceUri); + if (space && space.ownerDid !== sa.issuer) { + return c.json({ error: "Forbidden", reason: "not-owner" }, 403); + } + const ok = await spaces.revokeInvite(body.tokenHash); + return c.json({ ok }); + }); + + app.post(`/xrpc/${NS}.redeem`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as { token?: string } | null; + if (!body?.token) { + return c.json({ error: "InvalidRequest", message: "token required" }, 400); + } + const tokenHash = await hashInviteToken(body.token); + const now = Date.now(); + + // Try community first (it's atomic — returns null if not consumable). + if (community) { + const cinvite = await community.redeemInvite(tokenHash, now); + if (cinvite) { + const space = await spaces.getSpace(cinvite.spaceUri); + if (!space) { + return c.json({ error: "NotFound", reason: "space-not-found" }, 404); + } + // The token itself is the authorization: creator (manager+) pre-signed + // "anyone with this token gets level X". Grant directly, attributing + // to the creator so audit trails make sense. + await community.grant({ + spaceUri: cinvite.spaceUri, + subjectDid: sa.issuer, + accessLevel: cinvite.accessLevel, + grantedBy: cinvite.createdBy, + }); + await reconcile(community, spaces, cinvite.spaceUri, cinvite.createdBy); + return c.json({ + spaceUri: cinvite.spaceUri, + accessLevel: cinvite.accessLevel, + communityDid: space.ownerDid, + }); + } + } + + // Fall back to the spaces (user-owned) path. The spaces redeem filter + // already restricts to `kind IN ('join','read-join')` at the SQL level. + const sinvite = await spaces.redeemInvite(tokenHash, now); + if (!sinvite) { + return c.json({ error: "InvalidInvite", reason: "expired-revoked-or-exhausted" }, 400); + } + await spaces.addMember(sinvite.spaceUri, sa.issuer, sinvite.createdBy); + return c.json({ spaceUri: sinvite.spaceUri, kind: sinvite.kind }); + }); +} + +function getAuth(c: Context): ServiceAuth { + const a = c.get("serviceAuth") as ServiceAuth | undefined; + if (!a) throw new Error("service auth not set"); + return a; +} diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 74f0499..5636762 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -17,6 +17,7 @@ import type { CommunityRoutesOptions } from "../community/router"; import { CommunityAdapter } from "../community/adapter"; import { registerRealtimeRoutes } from "../realtime/router"; import type { RealtimeRoutesOptions } from "../realtime/router"; +import { registerInviteRoutes } from "../invite/router"; import { InMemoryPubSub } from "../realtime/in-memory"; import { wrapWithPublishing } from "../realtime/publishing-adapter"; import type { PubSub } from "../realtime/types"; @@ -116,7 +117,10 @@ export function createApp( registerCollectionRoutes(app, db, config, spacesCtx, { pubsub: realtimePubsub }); registerFeedRoutes(app, db, config); registerNotifyRoute(app, db, config); - registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx); + const communityAdapterForSpaces = config.community && spacesCtx + ? new CommunityAdapter(spacesDb) + : null; + registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx, communityAdapterForSpaces); const authOverride = config.spaces?.authOverride; @@ -135,6 +139,16 @@ export function createApp( ); } + if (config.spaces && spacesCtx) { + // Unified invite surface: one `.invite.*` family that dispatches on + // space ownership (user-owned → addMember; community-owned → grant). + const authMiddleware = + options.spaces?.authMiddleware ?? + createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); + const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; + registerInviteRoutes(app, config, spacesCtx.adapter, communityAdapter, { authMiddleware }); + } + if (config.realtime && realtimePubsub) { // The ticket endpoint still needs a JWT verifier — but that verifier only // exists when spaces is configured. Without spaces, private-topic ticket diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index a14c880..b955150 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -11,12 +11,11 @@ import { verifyServiceAuthRequest, } from "./auth"; import { nextTid } from "./tid"; -import { hashInviteToken, mintInviteToken } from "../invite/token"; +import { hashInviteToken } from "../invite/token"; +import { resolveEffectiveLevel } from "../community/acl"; import { buildSpaceUri } from "./uri"; import { DEFAULT_BLOB_MAX_SIZE, - type InviteKind, - type InviteRow, type SpaceRow, type SpacesConfig, type StorageAdapter, @@ -38,7 +37,8 @@ export function registerSpacesRoutes( db: Database, config: ContrailConfig, options: SpacesRoutesOptions = {}, - ctx?: { adapter: StorageAdapter; verifier: import("@atcute/xrpc-server/auth").ServiceJwtVerifier } | null + ctx?: { adapter: StorageAdapter; verifier: import("@atcute/xrpc-server/auth").ServiceJwtVerifier } | null, + community?: import("../community/adapter").CommunityAdapter | null ): void { const spacesConfig = config.spaces; if (!spacesConfig) return; @@ -486,89 +486,8 @@ export function registerSpacesRoutes( return c.json({ space: publicSpaceView(space, true) }); }); - // Invites (contrail extras — emitted under .spaceExt.invite.*) - app.post(`/xrpc/${SPACE_EXT}.invite.create`, auth, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as - | { - spaceUri?: string; - kind?: InviteKind; - expiresAt?: number; - maxUses?: number; - note?: string; - } - | null; - if (!body?.spaceUri) { - return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); - } - const kind: InviteKind = body.kind ?? "join"; - if (kind !== "join" && kind !== "read" && kind !== "read-join") { - return c.json({ error: "InvalidRequest", message: "kind must be 'join', 'read', or 'read-join'" }, 400); - } - const space = await adapter.getSpace(body.spaceUri); - if (!space) return c.json({ error: "NotFound" }, 404); - if (space.ownerDid !== sa.issuer) { - return c.json({ error: "Forbidden", reason: "not-owner" }, 403); - } - - const { token, tokenHash } = await mintInviteToken(); - const invite = await adapter.createInvite({ - spaceUri: body.spaceUri, - tokenHash, - kind, - expiresAt: body.expiresAt ?? null, - maxUses: body.maxUses ?? null, - createdBy: sa.issuer, - note: body.note ?? null, - }); - return c.json({ token, invite: publicInviteView(invite) }); - }); - - app.post(`/xrpc/${SPACE_EXT}.invite.redeem`, auth, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as { token?: string } | null; - if (!body?.token) { - return c.json({ error: "InvalidRequest", message: "token required" }, 400); - } - const tokenHash = await hashInviteToken(body.token); - const invite = await adapter.redeemInvite(tokenHash, Date.now()); - if (!invite) { - return c.json({ error: "InvalidInvite", reason: "expired-revoked-or-exhausted" }, 400); - } - await adapter.addMember(invite.spaceUri, sa.issuer, invite.createdBy); - return c.json({ spaceUri: invite.spaceUri }); - }); - - app.get(`/xrpc/${SPACE_EXT}.invite.list`, auth, async (c) => { - const sa = getAuth(c); - const spaceUri = c.req.query("spaceUri"); - if (!spaceUri) return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); - const space = await adapter.getSpace(spaceUri); - if (!space) return c.json({ error: "NotFound" }, 404); - if (space.ownerDid !== sa.issuer) { - return c.json({ error: "Forbidden", reason: "not-owner" }, 403); - } - const includeRevoked = c.req.query("includeRevoked") === "true"; - const invites = await adapter.listInvites(spaceUri, { includeRevoked }); - return c.json({ invites: invites.map(publicInviteView) }); - }); - - app.post(`/xrpc/${SPACE_EXT}.invite.revoke`, auth, async (c) => { - const sa = getAuth(c); - const body = (await c.req.json().catch(() => null)) as - | { spaceUri?: string; tokenHash?: string } - | null; - if (!body?.spaceUri || !body.tokenHash) { - return c.json({ error: "InvalidRequest", message: "spaceUri and tokenHash required" }, 400); - } - const space = await adapter.getSpace(body.spaceUri); - if (!space) return c.json({ error: "NotFound" }, 404); - if (space.ownerDid !== sa.issuer) { - return c.json({ error: "Forbidden", reason: "not-owner" }, 403); - } - const ok = await adapter.revokeInvite(body.tokenHash); - return c.json({ ok }); - }); + // Invites live under `.invite.*` — see src/core/invite/router.ts. The + // unified surface dispatches on space ownership. app.post(`/xrpc/${SPACE}.addMember`, auth, async (c) => { const sa = getAuth(c); @@ -625,6 +544,9 @@ export function registerSpacesRoutes( return c.json({ ok: true }); }); + // Unified whoami — `.spaceExt.whoami?spaceUri=X` → { isOwner, isMember, + // accessLevel? }. `accessLevel` is present only when the target space is + // community-owned; for user-owned spaces membership is binary. app.get(`/xrpc/${SPACE_EXT}.whoami`, auth, async (c) => { const sa = getAuth(c); const spaceUri = c.req.query("spaceUri"); @@ -633,12 +555,24 @@ export function registerSpacesRoutes( if (!space) return c.json({ error: "NotFound" }, 404); const isOwner = space.ownerDid === sa.issuer; - if (isOwner) { - return c.json({ isOwner: true, isMember: true }); + + // Community-owned space: resolve through the access-level ladder. The + // reconciler keeps spaces_members in sync, so isMember derives from the + // effective level directly. + const isCommunity = community ? !!(await community.getCommunity(space.ownerDid)) : false; + if (isCommunity) { + const level = await resolveEffectiveLevel(community!, spaceUri, sa.issuer); + return c.json({ + isOwner, + isMember: isOwner || !!level, + accessLevel: level, + }); } + + // User-owned space: binary membership. + if (isOwner) return c.json({ isOwner: true, isMember: true }); const member = await adapter.getMember(spaceUri, sa.issuer); - if (!member) return c.json({ isOwner: false, isMember: false }); - return c.json({ isOwner: false, isMember: true }); + return c.json({ isOwner: false, isMember: !!member }); }); } @@ -653,20 +587,6 @@ function getAuth(c: Parameters[0]): ServiceAuth { return auth; } -function publicInviteView(invite: InviteRow) { - return { - tokenHash: invite.tokenHash, - spaceUri: invite.spaceUri, - kind: invite.kind, - expiresAt: invite.expiresAt, - maxUses: invite.maxUses, - usedCount: invite.usedCount, - createdBy: invite.createdBy, - createdAt: invite.createdAt, - revokedAt: invite.revokedAt, - note: invite.note, - }; -} function publicSpaceView(space: SpaceRow, forOwner: boolean) { return { diff --git a/src/generate.ts b/src/generate.ts index 63e55d1..bf9ed21 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -64,7 +64,10 @@ function fieldToParam(field: string): string { * @atmo-dev/contrail. All three module templates live under one * `lexicon-templates/` parent; each subdir is instantiated by the generator * into its own `.*` namespace. */ -function findTemplatesDir(rootDir: string, module: "spaces" | "community" | "realtime"): string | null { +function findTemplatesDir( + rootDir: string, + module: "spaces" | "community" | "realtime" | "invite" +): string | null { const candidates = [ join(rootDir, "lexicon-templates", module), join(rootDir, "node_modules/@atmo-dev/contrail/lexicon-templates", module), @@ -78,6 +81,7 @@ function findTemplatesDir(rootDir: string, module: "spaces" | "community" | "rea const findSpaceTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "spaces"); const findCommunityTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "community"); const findRealtimeTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "realtime"); +const findInviteTemplatesDir = (rootDir: string) => findTemplatesDir(rootDir, "invite"); /** Yield all JSON files under a directory (recursive). */ function* walkJson(dir: string): Generator { @@ -893,6 +897,51 @@ export function generateLexicons(options: GenerateOptions): Record.invite.* family that dispatches on space + // ownership. Emitted whenever spaces is configured (user-owned spaces + // can always issue invites); community-owned spaces add the + // `accessLevel` branch inside the same handlers. --- + + if (config.spaces) { + log("Generating invite endpoints..."); + const templatesDir = findInviteTemplatesDir(rootDir); + if (!templatesDir) { + log(" (invite templates not found — skipping)"); + } else { + const templateIdRe = /^tools\.atmo\.invite(\.[A-Za-z0-9.]+)?$/; + const idReplace = (id: string) => + id.startsWith("tools.atmo.invite") + ? id.replace(/^tools\.atmo\.invite/, `${ns}.invite`) + : id; + + const rewriteRefs = (obj: any): any => { + if (Array.isArray(obj)) return obj.map(rewriteRefs); + if (obj && typeof obj === "object") { + const out: any = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === "ref" && typeof v === "string" && v.startsWith("tools.atmo.invite")) { + out[k] = v.replace(/^tools\.atmo\.invite/, `${ns}.invite`); + } else if (k === "id" && typeof v === "string" && templateIdRe.test(v)) { + out[k] = idReplace(v); + } else { + out[k] = rewriteRefs(v); + } + } + return out; + } + return obj; + }; + + for (const file of walkJson(templatesDir)) { + const doc = JSON.parse(readFileSync(file, "utf-8")); + if (typeof doc.id !== "string" || !templateIdRe.test(doc.id)) continue; + const newId = idReplace(doc.id); + const rewritten = rewriteRefs({ ...doc, id: newId }); + writeLexicon(newId, rewritten); + } + } + } + // --- Permission set --- // Permission-set lexicons (https://atproto.com/guides/permission-sets) can // only reference NSIDs under the same namespace as the set itself, which diff --git a/tests/community-delegation.test.ts b/tests/community-delegation.test.ts index bee3af4..7f9e58e 100644 --- a/tests/community-delegation.test.ts +++ b/tests/community-delegation.test.ts @@ -125,7 +125,7 @@ async function grant(app: Hono, caller: string, spaceUri: string, subject: any, } async function whoamiLevel(app: Hono, caller: string, spaceUri: string): Promise { - const res = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, caller); + const res = await call(app, "GET", `/xrpc/test.comm.spaceExt.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, caller); expect(res.status).toBe(200); return ((await res.json()) as any).accessLevel; } diff --git a/tests/community-e2e.test.ts b/tests/community-e2e.test.ts index f9d5338..84feb25 100644 --- a/tests/community-e2e.test.ts +++ b/tests/community-e2e.test.ts @@ -136,13 +136,13 @@ describe("community e2e — stage 1", () => { // whoami in both reserved spaces → owner for (const uri of [adminUri, publishersUri]) { - const r = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(uri)}`, ALICE); + const r = await call(app, "GET", `/xrpc/test.comm.spaceExt.whoami?spaceUri=${encodeURIComponent(uri)}`, ALICE); expect(r.status).toBe(200); expect(((await r.json()) as any).accessLevel).toBe("owner"); } // Bob isn't in either - const r = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(adminUri)}`, BOB); + const r = await call(app, "GET", `/xrpc/test.comm.spaceExt.whoami?spaceUri=${encodeURIComponent(adminUri)}`, BOB); expect(r.status).toBe(200); expect(((await r.json()) as any).accessLevel).toBe(null); }); @@ -255,7 +255,7 @@ describe("community e2e — stage 1", () => { const level = await call( app, "GET", - `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, + `/xrpc/test.comm.spaceExt.whoami?spaceUri=${encodeURIComponent(spaceUri)}`, BOB ); expect(((await level.json()) as any).accessLevel).toBe(null); diff --git a/tests/community-mint.test.ts b/tests/community-mint.test.ts index b573dfc..4e5e03d 100644 --- a/tests/community-mint.test.ts +++ b/tests/community-mint.test.ts @@ -150,7 +150,7 @@ describe("community.mint — stage 4", () => { // Reserved spaces exist with the caller as owner. const adminUri = `at://${body.communityDid}/tools.atmo.event.space/$admin`; - const whoami = await call(app, "GET", `/xrpc/test.comm.community.space.whoami?spaceUri=${encodeURIComponent(adminUri)}`, ALICE); + const whoami = await call(app, "GET", `/xrpc/test.comm.spaceExt.whoami?spaceUri=${encodeURIComponent(adminUri)}`, ALICE); expect(((await whoami.json()) as any).accessLevel).toBe("owner"); }); diff --git a/tests/invite-unified.test.ts b/tests/invite-unified.test.ts new file mode 100644 index 0000000..cafc755 --- /dev/null +++ b/tests/invite-unified.test.ts @@ -0,0 +1,213 @@ +/** Unified invite surface: `.invite.*` dispatches on space ownership. + * - user-owned space → `kind` in create, `addMember` on redeem. + * - community-owned → `accessLevel` in create, `grant`+`reconcile` on redeem. + * + * Both paths through a single endpoint family. */ + +import { describe, it, expect, beforeAll } from "vitest"; +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { createSqliteDatabase } from "../src/adapters/sqlite"; +import { initSchema } from "../src/core/db/schema"; +import { createApp } from "../src/core/router"; +import { resolveConfig } from "../src/core/types"; +import type { ContrailConfig } from "../src/core/types"; + +const ALICE = "did:plc:alice"; +const BOB = "did:plc:bob"; +const CHARLIE = "did:plc:charlie"; +const COMMUNITY_DID = "did:plc:acmecommunity"; +const PDS = "https://pds.example"; +const MASTER_KEY = new Uint8Array(32).fill(11); + +const CONFIG: ContrailConfig = { + namespace: "test.inv", + collections: { message: { collection: "app.event.message" } }, + spaces: { type: "tools.atmo.event.space", serviceDid: "did:web:test.example#svc" }, + community: { + masterKey: MASTER_KEY, + fetch: mockFetch, + resolver: mockResolver(), + }, +}; + +function mockResolver(): any { + return { + resolve: async (_did: string) => ({ + id: _did, + service: [{ id: "#atproto_pds", type: "AtprotoPersonalDataServer", serviceEndpoint: PDS }], + }), + }; +} + +async function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + if (url.endsWith("/xrpc/com.atproto.server.createSession") && init?.method === "POST") { + return new Response( + JSON.stringify({ accessJwt: "a.b.c", refreshJwt: "r.r.r", did: COMMUNITY_DID }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + return new Response("not found", { status: 404 }); +} + +function fakeAuth(): MiddlewareHandler { + return async (c, next) => { + const did = c.req.header("X-Test-Did"); + if (!did) return c.json({ error: "AuthRequired" }, 401); + c.set("serviceAuth", { issuer: did, audience: CONFIG.spaces!.serviceDid, lxm: undefined }); + await next(); + }; +} + +async function makeApp(): Promise { + const db = createSqliteDatabase(":memory:"); + const resolved = resolveConfig(CONFIG); + await initSchema(db, resolved); + return createApp(db, resolved, { spaces: { authMiddleware: fakeAuth() } }); +} + +function call(app: Hono, method: string, path: string, did: string, body?: any): Promise { + const headers: Record = { "X-Test-Did": did }; + if (body !== undefined) headers["Content-Type"] = "application/json"; + return app.fetch( + new Request(`http://localhost${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + ); +} + +describe(".invite.* — unified surface", () => { + describe("user-owned space (kind)", () => { + let app: Hono; + let spaceUri: string; + + beforeAll(async () => { + app = await makeApp(); + const res = await call(app, "POST", "/xrpc/test.inv.space.createSpace", ALICE, { key: "lounge" }); + expect(res.status).toBe(200); + spaceUri = ((await res.json()) as any).space.uri; + }); + + it("rejects accessLevel on a user-owned space", async () => { + const res = await call(app, "POST", "/xrpc/test.inv.invite.create", ALICE, { + spaceUri, + accessLevel: "member", + }); + expect(res.status).toBe(400); + expect(((await res.json()) as any).reason).toBe("accessLevel-on-user-space"); + }); + + it("owner creates kind=join; redeemer becomes a member", async () => { + const create = await call(app, "POST", "/xrpc/test.inv.invite.create", ALICE, { spaceUri }); + expect(create.status).toBe(200); + const { token, invite } = (await create.json()) as any; + expect(invite.kind).toBe("join"); + expect(invite.accessLevel).toBeUndefined(); + + const redeem = await call(app, "POST", "/xrpc/test.inv.invite.redeem", BOB, { token }); + expect(redeem.status).toBe(200); + const body = (await redeem.json()) as any; + expect(body.spaceUri).toBe(spaceUri); + expect(body.kind).toBe("join"); + expect(body.accessLevel).toBeUndefined(); + }); + }); + + describe("community-owned space (accessLevel)", () => { + let app: Hono; + let adminUri: string; + let channelUri: string; + + beforeAll(async () => { + app = await makeApp(); + // Adopt a community owned by Alice. + const adopt = await call(app, "POST", "/xrpc/test.inv.community.adopt", ALICE, { + identifier: COMMUNITY_DID, + appPassword: "anything", + }); + expect(adopt.status).toBe(200); + adminUri = `at://${COMMUNITY_DID}/tools.atmo.event.space/$admin`; + + // Alice (owner in $admin) creates a child space. + const create = await call(app, "POST", "/xrpc/test.inv.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "general", + }); + expect(create.status).toBe(200); + channelUri = ((await create.json()) as any).space.uri; + }); + + it("rejects kind on a community-owned space", async () => { + const res = await call(app, "POST", "/xrpc/test.inv.invite.create", ALICE, { + spaceUri: channelUri, + kind: "join", + }); + expect(res.status).toBe(400); + expect(((await res.json()) as any).reason).toBe("kind-on-community-space"); + }); + + it("requires accessLevel", async () => { + const res = await call(app, "POST", "/xrpc/test.inv.invite.create", ALICE, { + spaceUri: channelUri, + }); + expect(res.status).toBe(400); + expect(((await res.json()) as any).reason).toBe("accessLevel-required"); + }); + + it("owner creates with accessLevel=member; redeemer is granted that level", async () => { + const create = await call(app, "POST", "/xrpc/test.inv.invite.create", ALICE, { + spaceUri: channelUri, + accessLevel: "member", + }); + expect(create.status).toBe(200); + const { token, invite } = (await create.json()) as any; + expect(invite.accessLevel).toBe("member"); + expect(invite.kind).toBeUndefined(); + + const redeem = await call(app, "POST", "/xrpc/test.inv.invite.redeem", BOB, { token }); + expect(redeem.status).toBe(200); + const body = (await redeem.json()) as any; + expect(body.spaceUri).toBe(channelUri); + expect(body.accessLevel).toBe("member"); + expect(body.communityDid).toBe(COMMUNITY_DID); + + // Bob now shows up on whoami via the ladder. + const w = await call(app, "GET", `/xrpc/test.inv.spaceExt.whoami?spaceUri=${encodeURIComponent(channelUri)}`, BOB); + expect(((await w.json()) as any).accessLevel).toBe("member"); + }); + + it("non-manager cannot create an invite on a community space", async () => { + const res = await call(app, "POST", "/xrpc/test.inv.invite.create", CHARLIE, { + spaceUri: channelUri, + accessLevel: "member", + }); + expect(res.status).toBe(403); + expect(((await res.json()) as any).reason).toBe("manager-required"); + }); + + it("cannot grant a higher access level than caller's own", async () => { + // Create a second space. Grant Charlie as "manager" there, then have him + // try to mint an "admin" invite — should fail. + const create = await call(app, "POST", "/xrpc/test.inv.community.space.create", ALICE, { + communityDid: COMMUNITY_DID, + key: "side", + }); + const sideUri = ((await create.json()) as any).space.uri; + await call(app, "POST", "/xrpc/test.inv.community.space.grant", ALICE, { + spaceUri: sideUri, + subject: { did: CHARLIE }, + accessLevel: "manager", + }); + + const res = await call(app, "POST", "/xrpc/test.inv.invite.create", CHARLIE, { + spaceUri: sideUri, + accessLevel: "admin", + }); + expect(res.status).toBe(403); + expect(((await res.json()) as any).reason).toBe("cannot-grant-higher-than-self"); + }); + }); +}); diff --git a/tests/spaces-invites.test.ts b/tests/spaces-invites.test.ts index 82f58f7..eb3fc1c 100644 --- a/tests/spaces-invites.test.ts +++ b/tests/spaces-invites.test.ts @@ -84,14 +84,14 @@ describe("invite e2e", () => { }); it("non-owner cannot create an invite", async () => { - const res = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", BOB, { + const res = await call(app, "POST", "/xrpc/test.spaces.invite.create", BOB, { spaceUri, }); expect(res.status).toBe(403); }); it("owner creates an invite and Bob redeems it to become a member", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, }); expect(create.status).toBe(200); @@ -101,7 +101,7 @@ describe("invite e2e", () => { expect(invite.spaceUri).toBe(spaceUri); expect(invite.usedCount).toBe(0); - const redeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", BOB, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", BOB, { token }); expect(redeem.status).toBe(200); const body = (await redeem.json()) as any; expect(body.spaceUri).toBe(spaceUri); @@ -116,58 +116,58 @@ describe("invite e2e", () => { }); it("single-use invite rejects second redemption", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, maxUses: 1, }); const { token } = (await create.json()) as any; - const first = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", BOB, { token }); + const first = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", BOB, { token }); expect(first.status).toBe(200); - const second = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); + const second = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", CHARLIE, { token }); expect(second.status).toBe(400); const body = (await second.json()) as any; expect(body.reason).toBe("expired-revoked-or-exhausted"); }); it("expired invite rejects redemption", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, expiresAt: Date.now() - 1000, }); const { token } = (await create.json()) as any; - const res = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); + const res = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", CHARLIE, { token }); expect(res.status).toBe(400); }); it("revoked invite rejects redemption and list filters it by default", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, }); const { token, invite } = (await create.json()) as any; - const revoke = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.revoke", ALICE, { + const revoke = await call(app, "POST", "/xrpc/test.spaces.invite.revoke", ALICE, { spaceUri, tokenHash: invite.tokenHash, }); expect(revoke.status).toBe(200); expect(((await revoke.json()) as any).ok).toBe(true); - const tryRedeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); + const tryRedeem = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", CHARLIE, { token }); expect(tryRedeem.status).toBe(400); - const listActive = await call(app, "GET", `/xrpc/test.spaces.spaceExt.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, ALICE); + const listActive = await call(app, "GET", `/xrpc/test.spaces.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, ALICE); const activeHashes = ((await listActive.json()) as any).invites.map((i: any) => i.tokenHash); expect(activeHashes).not.toContain(invite.tokenHash); - const listAll = await call(app, "GET", `/xrpc/test.spaces.spaceExt.invite.list?spaceUri=${encodeURIComponent(spaceUri)}&includeRevoked=true`, ALICE); + const listAll = await call(app, "GET", `/xrpc/test.spaces.invite.list?spaceUri=${encodeURIComponent(spaceUri)}&includeRevoked=true`, ALICE); const allHashes = ((await listAll.json()) as any).invites.map((i: any) => i.tokenHash); expect(allHashes).toContain(invite.tokenHash); }); it("non-owner cannot list or revoke invites", async () => { - const listRes = await call(app, "GET", `/xrpc/test.spaces.spaceExt.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, BOB); + const listRes = await call(app, "GET", `/xrpc/test.spaces.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, BOB); expect(listRes.status).toBe(403); - const revokeRes = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.revoke", BOB, { + const revokeRes = await call(app, "POST", "/xrpc/test.spaces.invite.revoke", BOB, { spaceUri, tokenHash: "nonexistent", }); expect(revokeRes.status).toBe(403); @@ -179,7 +179,7 @@ describe("invite e2e", () => { spaceUri, collection: "app.event.message", record: { text: "hello world" }, }); - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, kind: "read", }); const { token, invite } = (await create.json()) as any; @@ -193,12 +193,12 @@ describe("invite e2e", () => { expect(body.records.length).toBeGreaterThan(0); // Read token cannot be redeemed for membership. - const redeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", CHARLIE, { token }); expect(redeem.status).toBe(400); }); it("read-join token grants anonymous read AND can be redeemed for membership", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, kind: "read-join", }); const { token, invite } = (await create.json()) as any; @@ -210,16 +210,16 @@ describe("invite e2e", () => { expect(anon.status).toBe(200); // Same token can also be redeemed by a signed-in user. - const redeem = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.redeem", CHARLIE, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.invite.redeem", CHARLIE, { token }); expect(redeem.status).toBe(200); }); it("revoked read-token rejects anonymous read", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, kind: "read", }); const { token, invite } = (await create.json()) as any; - await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.revoke", ALICE, { + await call(app, "POST", "/xrpc/test.spaces.invite.revoke", ALICE, { spaceUri, tokenHash: invite.tokenHash, }); const url = `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.message&inviteToken=${token}`; @@ -228,7 +228,7 @@ describe("invite e2e", () => { }); it("read-token does not grant write", async () => { - const create = await call(app, "POST", "/xrpc/test.spaces.spaceExt.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.invite.create", ALICE, { spaceUri, kind: "read", }); const { token } = (await create.json()) as any; -- 2.51.2 From caca8c86daea4b1796e20941f282865cdcf0260e Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:46:11 +0200 Subject: [PATCH 12/18] improve auth for sync --- .../src/lib/contrail/index.ts | 22 ++-- .../.claude/settings.local.json | 5 +- examples/sveltekit-group-chat/src/app.d.ts | 1 - .../src/lib/atproto/server/signed-cookie.ts | 37 ------ .../src/lib/contrail/index.ts | 52 +++----- .../src/lib/rooms/rooms.remote.ts | 17 +++ .../src/lib/rooms/server.ts | 111 +++++++----------- .../src/routes/api/blob-upload/+server.ts | 5 +- .../src/routes/api/blob/+server.ts | 34 ++---- .../src/routes/api/ticket/+server.ts | 9 +- .../src/routes/api/watch-token/+server.ts | 25 ---- .../[channelKey]/+page.server.ts | 29 ++++- .../[communityDid]/[channelKey]/+page.svelte | 35 +++--- .../settings/invites/+page.server.ts | 9 +- src/core/router/collection.ts | 16 +-- src/core/router/index.ts | 8 +- src/core/spaces/auth.ts | 75 +++++------- src/core/spaces/in-process.ts | 34 ++++++ src/core/spaces/router.ts | 6 +- src/core/spaces/types.ts | 18 --- src/index.ts | 1 - src/server.ts | 32 +++++ src/sync/index.ts | 33 ++---- 23 files changed, 263 insertions(+), 351 deletions(-) delete mode 100644 examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts create mode 100644 src/core/spaces/in-process.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/contrail/index.ts b/examples/sveltekit-cloudflare-workers/src/lib/contrail/index.ts index aa8c784..7aaf747 100644 --- a/examples/sveltekit-cloudflare-workers/src/lib/contrail/index.ts +++ b/examples/sveltekit-cloudflare-workers/src/lib/contrail/index.ts @@ -1,6 +1,6 @@ import { Contrail } from '@atmo-dev/contrail'; -import { createHandler } from '@atmo-dev/contrail/server'; -import { Client } from '@atcute/client'; +import { createHandler, createServerClient } from '@atmo-dev/contrail/server'; +import type { Client } from '@atcute/client'; import { config } from './config'; export const contrail = new Contrail(config); @@ -17,15 +17,13 @@ export async function ensureInit(db: D1Database) { const handle = createHandler(contrail); /** - * Server-side: fully typed @atcute/client that routes through contrail in-process. - * No HTTP roundtrip — calls createHandler directly. + * Typed `@atcute/client` that calls contrail in-process. Pass `did` to act + * as that user (server-side principal via WeakMap marker — no JWT, no PDS + * roundtrip). Omit for anonymous calls against public endpoints. */ -export function getServerClient(db: D1Database) { - return new Client({ - handler: async (pathname, init) => { - await ensureInit(db); - const url = new URL(pathname, 'http://localhost'); - return handle(new Request(url, init), db) as Promise; - } - }); +export function getServerClient(db: D1Database, did?: string): Client { + return createServerClient(async (req) => { + await ensureInit(db); + return handle(req, db) as Promise; + }, did); } diff --git a/examples/sveltekit-group-chat/.claude/settings.local.json b/examples/sveltekit-group-chat/.claude/settings.local.json index 32f6420..b9ddfaa 100644 --- a/examples/sveltekit-group-chat/.claude/settings.local.json +++ b/examples/sveltekit-group-chat/.claude/settings.local.json @@ -6,7 +6,10 @@ "mcp__plugin_svelte_svelte__get-documentation", "Bash(pnpm check:*)", "Bash(pnpm env:generate-secret:*)", - "Bash(pnpm build:agent-setup:*)" + "Bash(pnpm build:agent-setup:*)", + "Read(//Users/florian/contrail/**)", + "Bash(pnpm typecheck *)", + "Bash(pnpm test *)" ] } } diff --git a/examples/sveltekit-group-chat/src/app.d.ts b/examples/sveltekit-group-chat/src/app.d.ts index 35167b9..9190b9d 100644 --- a/examples/sveltekit-group-chat/src/app.d.ts +++ b/examples/sveltekit-group-chat/src/app.d.ts @@ -28,7 +28,6 @@ declare global { COMMUNITY_MASTER_KEY: string; REALTIME_TICKET_SECRET: string; SERVICE_DID: string; - DEV_AUTH?: string; REALTIME: DurableObjectNamespace; BLOBS?: R2Bucket; }; diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts b/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts index 7f63f5d..6cde19c 100644 --- a/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts +++ b/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts @@ -67,40 +67,3 @@ export function setSignedCookie( const signed = `${value}${SEPARATOR}${sig}`; cookies.set(name, signed, options); } - -/** Verify + read a signed cookie from a raw Request's Cookie header. - * Used from places that don't have a SvelteKit `Cookies` instance - * (e.g. contrail auth override). */ -export function getSignedCookieFromRequest(req: Request, name: string): string | null { - const header = req.headers.get('cookie'); - if (!header) return null; - const pairs = header.split(/;\s*/); - let raw: string | null = null; - for (const pair of pairs) { - const eq = pair.indexOf('='); - if (eq === -1) continue; - const key = pair.slice(0, eq); - if (key === name) { - raw = decodeURIComponent(pair.slice(eq + 1)); - break; - } - } - if (!raw) return null; - - const idx = raw.lastIndexOf(SEPARATOR); - if (idx === -1) return null; - const value = raw.slice(0, idx); - const sig = raw.slice(idx + 1); - - let expected: Uint8Array; - let got: Uint8Array; - try { - expected = hmacSha256(value); - got = fromBase64Url(sig); - } catch { - return null; - } - - if (got.length !== expected.length || !timingSafeEqual(got, expected)) return null; - return value; -} diff --git a/examples/sveltekit-group-chat/src/lib/contrail/index.ts b/examples/sveltekit-group-chat/src/lib/contrail/index.ts index 13f2473..555d348 100644 --- a/examples/sveltekit-group-chat/src/lib/contrail/index.ts +++ b/examples/sveltekit-group-chat/src/lib/contrail/index.ts @@ -10,10 +10,10 @@ import { type PubSub, type R2BucketLike } from '@atmo-dev/contrail'; -import { createHandler } from '@atmo-dev/contrail/server'; -import { Client } from '@atcute/client'; +import { createHandler, createServerClient } from '@atmo-dev/contrail/server'; +import type { Client } from '@atcute/client'; +import { dev } from '$app/environment'; import { baseConfig } from './config'; -import { getSignedCookieFromRequest } from '$lib/atproto/server/signed-cookie'; type Env = App.Platform['env']; @@ -26,13 +26,11 @@ interface Bundle { let cached: { env: Env; bundle: Bundle } | null = null; function build(env: Env): Bundle { - const devAuth = env.DEV_AUTH === '1'; - // In dev the DO class isn't wired (vite runs the worker directly, skipping // the post-build re-export), so fall back to InMemoryPubSub. Single-isolate // is fine for one-user dev; production uses the DO so publishes fan out // across isolates. - const pubsub: PubSub = devAuth + const pubsub: PubSub = dev ? new InMemoryPubSub() : new DurableObjectPubSub(env.REALTIME as DurableObjectNamespace); @@ -51,23 +49,7 @@ function build(env: Env): Bundle { adapter: blobAdapter, maxSize: 2 * 1024 * 1024, accept: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] - }, - // Dev-only bypass: trust the HMAC-signed session cookie (or an - // X-Dev-Did header for server-side synthetic requests) as auth, so - // the loopback OAuth client doesn't need to round-trip through the - // user's PDS for getServiceAuth (bsky.social rejects that on - // loopback). In prod, DEV_AUTH is unset and every caller — browser - // or third-party — must present a real atproto service-auth JWT. - // Browsers get one by calling our SvelteKit helper which uses the - // OAuth session to mint via the user's PDS. - authOverride: devAuth - ? (req: Request) => { - const headerDid = req.headers.get('x-dev-did'); - const did = headerDid ?? getSignedCookieFromRequest(req, 'did'); - if (!did) return null; - return { issuer: did, audience: env.SERVICE_DID }; - } - : undefined + } }, community: { masterKey: env.COMMUNITY_MASTER_KEY, @@ -107,19 +89,13 @@ export async function dispatch(req: Request, env: Env): Promise { return (await b.handle(req, env.DB)) as Response; } -/** Server-side typed @atcute client that routes through contrail in-process. - * When `asDid` is set, every request carries an Authorization header identifying - * the caller — this is used only for server-originated bootstrap calls (e.g. the - * community mint flow) where we already have the user's DID from the OAuth session - * but want to bypass the atproto PDS round-trip. For normal user actions, route - * through the user's OAuth client instead. */ -export function getServerClient(env: Env): Client { - return new Client({ - handler: async (pathname, init) => { - const b = getBundle(env); - await b.ready; - const url = new URL(pathname, 'http://localhost'); - return (await b.handle(new Request(url, init), env.DB)) as Response; - } - }); +/** Typed `@atcute/client` that calls contrail in-process. Pass `did` to act + * as that user (server-side principal via in-process WeakMap — no JWT, no + * PDS roundtrip). Omit for anonymous calls against public endpoints. */ +export function getServerClient(env: Env, did?: string): Client { + return createServerClient(async (req) => { + const b = getBundle(env); + await b.ready; + return (await b.handle(req, env.DB)) as Response; + }, did); } diff --git a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts index f61a27c..23cfefb 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts @@ -11,6 +11,7 @@ import { communityPutRecord, grantAccess, mintCommunity, + mintWatchTicket, spacePutRecord, revokeAccess, setAccessLevel, @@ -178,6 +179,22 @@ export const postMessage = command(PostMessageInput, async (input) => { return res; }); +// --------------------------------------------------------------------------- +// mintWatchTicket — browser calls on reconnect to get a fresh watch-scoped +// ticket. The initial ticket is pre-minted in +page.server.ts. +// --------------------------------------------------------------------------- + +const MintWatchTicketInput = v.object({ + spaceUri: v.pipe(v.string(), v.minLength(1)), + watchRecordsNsid: v.pipe(v.string(), v.minLength(1)), + limit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(200))) +}); + +export const mintWatchTicketCmd = command(MintWatchTicketInput, async (input) => { + const ctx = requireAuthed(); + return mintWatchTicket(ctx, input); +}); + // --------------------------------------------------------------------------- // grantMember / revokeMember / setMemberLevel — used in settings UI. // --------------------------------------------------------------------------- diff --git a/examples/sveltekit-group-chat/src/lib/rooms/server.ts b/examples/sveltekit-group-chat/src/lib/rooms/server.ts index 0aa444a..c81e4ac 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/server.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/server.ts @@ -1,27 +1,23 @@ /** Server-side helpers for calling contrail's authenticated XRPCs. * - * Contrail's community/space/realtime endpoints authenticate via atproto - * service-auth JWTs. The caller's OAuth session on their PDS can mint those - * JWTs via `com.atproto.server.getServiceAuth`. This module wraps that dance: - * mint → dispatch → return the typed body. - */ + * All calls route through contrail in-process using the WeakMap-backed + * in-process auth marker (see `@atmo-dev/contrail/server` → `markInProcess`). + * No JWT minting, no PDS roundtrip — the principal is stamped onto the + * Request by `ctx.did` and the auth middleware reads it directly. */ -import type { Client } from '@atcute/client'; +import { markInProcess } from '@atmo-dev/contrail/server'; import { dispatch } from '$lib/contrail'; type Env = App.Platform['env']; interface AuthedCallContext { env: Env; - /** Caller's OAuth-authenticated atproto client (talks to their PDS). */ - client: Client; - /** Caller's DID. */ + /** Caller's DID — the principal attributed to each request. */ did: string; } -/** Authenticated fetch for page loaders — mints a service-auth JWT - * (or takes the dev bypass) and dispatches to contrail in-process. - * Returns the parsed JSON body, or throws with status + payload. */ +/** Authenticated fetch for page loaders. Returns the parsed JSON body, + * or throws with status + payload on non-2xx. */ export async function authedFetch( ctx: AuthedCallContext, method: string, @@ -30,53 +26,22 @@ export async function authedFetch( return callContrail(ctx, method, opts); } -async function mintServiceAuth( - ctx: AuthedCallContext, - lxm: string, - expSeconds = 60 -): Promise { - // Dev bypass: the contrail authOverride trusts our signed session cookie - // instead of a real JWT, so we don't need to hit the PDS. Returns a sentinel - // the override ignores (any string is fine — it's never verified). - if (ctx.env.DEV_AUTH === '1') { - return 'dev'; - } - const res = await ctx.client.get('com.atproto.server.getServiceAuth', { - params: { - aud: ctx.env.SERVICE_DID as `did:${string}:${string}`, - lxm: lxm as `${string}.${string}.${string}`, - exp: Math.floor(Date.now() / 1000) + expSeconds - } - }); - if (!res.ok) { - throw new Error(`getServiceAuth failed: ${JSON.stringify(res.data)}`); - } - const data = res.data as { token: string }; - return data.token; -} - async function callContrail( ctx: AuthedCallContext, method: string, opts: { body?: unknown; query?: Record; httpMethod?: 'GET' | 'POST' } ): Promise { - const jwt = await mintServiceAuth(ctx, method); const url = new URL(`http://localhost/xrpc/${method}`); for (const [k, v] of Object.entries(opts.query ?? {})) { url.searchParams.set(k, v); } const httpMethod = opts.httpMethod ?? (opts.body === undefined ? 'GET' : 'POST'); - const devAuth = ctx.env.DEV_AUTH === '1'; - const headers: Record = { - Authorization: `Bearer ${jwt}`, - ...(opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}) - }; - if (devAuth) headers['X-Dev-Did'] = ctx.did; const req = new Request(url, { method: httpMethod, - headers, + headers: opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined }); + markInProcess(req, ctx.did); const res = await dispatch(req, ctx.env); const text = await res.text(); const json = text ? JSON.parse(text) : {}; @@ -225,6 +190,33 @@ export async function getRealtimeTicket( return callContrail(ctx, 'tools.atmo.chat.realtime.ticket', { body: input }); } +/** Mint a watch-scoped ticket for a `.watchRecords` stream by + * running the `mode=ws` handshake in-process. The response's `ticket` carries + * the (did, topic, querySpec) binding that watchRecords verifies on SSE or + * WS connect, so the browser can attach it as `?ticket=...` with no other + * auth. The snapshot body is discarded — the sync engine will run its own + * snapshot on the ticketed connection. */ +export async function mintWatchTicket( + ctx: AuthedCallContext, + input: { watchRecordsNsid: string; spaceUri: string; limit?: number } +): Promise<{ ticket: string; expiresAt: number }> { + const url = new URL(`http://localhost/xrpc/${input.watchRecordsNsid}`); + url.searchParams.set('spaceUri', input.spaceUri); + url.searchParams.set('mode', 'ws'); + if (input.limit) url.searchParams.set('limit', String(input.limit)); + const req = new Request(url, { headers: { accept: 'application/json' } }); + markInProcess(req, ctx.did); + const res = await dispatch(req, ctx.env); + const text = await res.text(); + if (!res.ok) throw new Error(`mintWatchTicket failed (${res.status}): ${text}`); + const data = JSON.parse(text) as { ticket?: string; ticketTtlMs?: number }; + if (!data.ticket) throw new Error('mintWatchTicket: handshake did not return a ticket'); + return { + ticket: data.ticket, + expiresAt: Date.now() + (data.ticketTtlMs ?? 120_000) + }; +} + // --- invites --------------------------------------------------------------- export interface InviteView { @@ -301,40 +293,21 @@ export async function uploadBlob( input: { spaceUri: string; mimeType: string; bytes: Uint8Array } ): Promise { const method = 'tools.atmo.chat.space.uploadBlob'; - const jwt = ctx.env.DEV_AUTH === '1' ? 'dev' : await mintServiceAuthForUpload(ctx, method); const url = new URL(`http://localhost/xrpc/${method}`); url.searchParams.set('spaceUri', input.spaceUri); - const headers: Record = { - Authorization: `Bearer ${jwt}`, - 'Content-Type': input.mimeType, - 'Content-Length': String(input.bytes.byteLength) - }; - if (ctx.env.DEV_AUTH === '1') headers['X-Dev-Did'] = ctx.did; const req = new Request(url, { method: 'POST', - headers, + headers: { + 'Content-Type': input.mimeType, + 'Content-Length': String(input.bytes.byteLength) + }, body: new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }) }); + markInProcess(req, ctx.did); const res = await dispatch(req, ctx.env); const text = await res.text(); const json = text ? JSON.parse(text) : {}; if (!res.ok) throw new Error(`${method} failed (${res.status}): ${text}`); return (json as { blob: BlobRef }).blob; } - -async function mintServiceAuthForUpload( - ctx: AuthedCallContext, - lxm: string, - expSeconds = 60 -): Promise { - const res = await ctx.client.get('com.atproto.server.getServiceAuth', { - params: { - aud: ctx.env.SERVICE_DID as `did:${string}:${string}`, - lxm: lxm as `${string}.${string}.${string}`, - exp: Math.floor(Date.now() / 1000) + expSeconds - } - }); - if (!res.ok) throw new Error('getServiceAuth failed'); - return (res.data as { token: string }).token; -} diff --git a/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts b/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts index d92ca75..de5b9ee 100644 --- a/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts +++ b/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts @@ -1,6 +1,5 @@ import { error, json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import type { Client } from '@atcute/client'; import { uploadBlob } from '$lib/rooms/server'; /** POST raw bytes (body = bytes) with ?spaceUri=&mimeType=. @@ -8,7 +7,7 @@ import { uploadBlob } from '$lib/rooms/server'; * Plain +server.ts so we can pass binary without remote-function JSON schema, * and so there's no auto-revalidation. */ export const POST: RequestHandler = async ({ request, url, locals, platform }) => { - if (!locals.did || !locals.client) error(401, 'Not authenticated'); + if (!locals.did) error(401, 'Not authenticated'); const spaceUri = url.searchParams.get('spaceUri'); const mimeType = url.searchParams.get('mimeType') ?? request.headers.get('content-type'); if (!spaceUri) error(400, 'spaceUri required'); @@ -16,7 +15,7 @@ export const POST: RequestHandler = async ({ request, url, locals, platform }) = const bytes = new Uint8Array(await request.arrayBuffer()); const blob = await uploadBlob( - { env: platform!.env, client: locals.client as Client, did: locals.did as string }, + { env: platform!.env, did: locals.did as string }, { spaceUri, mimeType, bytes } ); return json(blob); diff --git a/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts b/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts index 20031d4..387f29f 100644 --- a/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts +++ b/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts @@ -1,12 +1,14 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; +import { markInProcess } from '@atmo-dev/contrail/server'; import { dispatch } from '$lib/contrail'; -import { getSignedCookie } from '$lib/atproto/server/signed-cookie'; /** GET ?spaceUri=&cid= → blob bytes. - * Proxies through contrail's space.getBlob endpoint with the caller's - * (cookie-based in dev / JWT in prod) auth attached. */ -export const GET: RequestHandler = async ({ url, locals, platform, request, cookies }) => { + * Proxies through contrail's space.getBlob endpoint, attributing the call + * to the session's DID via the in-process auth marker. Browsers can't + * attach headers to ``, which is why this proxy exists. */ +export const GET: RequestHandler = async ({ url, locals, platform }) => { + if (!locals.did) error(401, 'Not authenticated'); const spaceUri = url.searchParams.get('spaceUri'); const cid = url.searchParams.get('cid'); if (!spaceUri || !cid) error(400, 'spaceUri and cid required'); @@ -15,27 +17,9 @@ export const GET: RequestHandler = async ({ url, locals, platform, request, cook target.searchParams.set('spaceUri', spaceUri); target.searchParams.set('cid', cid); - // Forward the session cookie so dev auth override works; in prod the - // caller's OAuth session can mint a real JWT — but for we can't - // attach headers from the browser, so we mint server-side. - const headers: Record = {}; - if (platform!.env.DEV_AUTH === '1') { - const did = getSignedCookie(cookies, 'did'); - if (did) headers['X-Dev-Did'] = did; - } else { - if (!locals.client || !locals.did) error(401, 'Not authenticated'); - const authRes = await locals.client.get('com.atproto.server.getServiceAuth', { - params: { - aud: platform!.env.SERVICE_DID as `did:${string}:${string}`, - lxm: 'tools.atmo.chat.space.getBlob' as `${string}.${string}.${string}`, - exp: Math.floor(Date.now() / 1000) + 120 - } - }); - if (authRes.ok) headers.Authorization = `Bearer ${(authRes.data as { token: string }).token}`; - } - - const proxied = await dispatch(new Request(target, { headers }), platform!.env); - // Pass through status + body + content-type. + const req = new Request(target); + markInProcess(req, locals.did); + const proxied = await dispatch(req, platform!.env); return new Response(proxied.body, { status: proxied.status, headers: { diff --git a/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts b/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts index 9408598..f28d926 100644 --- a/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts +++ b/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts @@ -1,6 +1,5 @@ import { error, json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import type { Client } from '@atcute/client'; import { getRealtimeTicket } from '$lib/rooms/server'; /** POST { topic } → realtime ticket. @@ -8,16 +7,12 @@ import { getRealtimeTicket } from '$lib/rooms/server'; * calling it doesn't trigger SvelteKit's auto-revalidation (which would * cycle with the layout effect that opens the EventSource). */ export const POST: RequestHandler = async ({ request, locals, platform }) => { - if (!locals.did || !locals.client) error(401, 'Not authenticated'); + if (!locals.did) error(401, 'Not authenticated'); const body = (await request.json().catch(() => null)) as { topic?: string } | null; if (!body?.topic) error(400, 'topic required'); const res = await getRealtimeTicket( - { - env: platform!.env, - client: locals.client as Client, - did: locals.did as string - }, + { env: platform!.env, did: locals.did as string }, { topic: body.topic } ); return json(res); diff --git a/examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts b/examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts deleted file mode 100644 index a54999c..0000000 --- a/examples/sveltekit-group-chat/src/routes/api/watch-token/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -/** POST { lxm } → { token } — mint an atproto service-auth JWT scoped to one - * XRPC method, using the caller's OAuth session to call getServiceAuth on - * their PDS. Used by the browser sync engine to auth the watchRecords - * handshake fetch — it can't mint JWTs itself, but same-origin to us it - * can delegate. - * - * Cross-origin apps skip this and mint their own JWTs server-side. */ -export const POST: RequestHandler = async ({ request, locals, platform }) => { - if (!locals.did || !locals.client) error(401, 'Not authenticated'); - const body = (await request.json().catch(() => null)) as { lxm?: string } | null; - if (!body?.lxm) error(400, 'lxm required'); - - const res = await locals.client.get('com.atproto.server.getServiceAuth', { - params: { - aud: platform!.env.SERVICE_DID as `did:${string}:${string}`, - lxm: body.lxm as `${string}.${string}.${string}`, - exp: Math.floor(Date.now() / 1000) + 120 - } - }); - if (!res.ok) error(502, `getServiceAuth failed: ${JSON.stringify(res.data)}`); - return json({ token: (res.data as { token: string }).token }); -}; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts index 5985ba5..e05db66 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts @@ -1,10 +1,33 @@ import type { PageServerLoad } from './$types'; import { error } from '@sveltejs/kit'; import { buildSpaceUri } from '$lib/rooms/uri'; +import { mintWatchTicket } from '$lib/rooms/server'; -export const load: PageServerLoad = async ({ locals, params }) => { - if (!locals.did || !locals.client) error(401, 'Not authenticated'); +const WATCH_RECORDS_NSID = 'tools.atmo.chat.message.watchRecords'; + +export const load: PageServerLoad = async ({ locals, params, platform }) => { + if (!locals.did) error(401, 'Not authenticated'); const communityDid = decodeURIComponent(params.communityDid); const spaceUri = buildSpaceUri(communityDid, params.channelKey); - return { spaceUri, channelKey: params.channelKey, myDid: locals.did }; + + // Mint the first watch-scoped ticket alongside the page data so the browser + // can open the stream on first paint without a second roundtrip. Reconnects + // call the `mintWatchTicket` remote function. + let initialTicket: string | null = null; + try { + const t = await mintWatchTicket( + { env: platform!.env, did: locals.did as string }, + { watchRecordsNsid: WATCH_RECORDS_NSID, spaceUri, limit: 50 } + ); + initialTicket = t.ticket; + } catch { + // Non-fatal: the browser will mint on demand via the remote function. + } + + return { + spaceUri, + channelKey: params.channelKey, + myDid: locals.did, + initialTicket + }; }; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte index 4814463..616dafd 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte @@ -2,7 +2,7 @@ import { tick } from 'svelte'; import { Button, Input, Navbar } from '@foxui/core'; import { RelativeTime } from '@foxui/time'; - import { postMessage } from '$lib/rooms/rooms.remote'; + import { postMessage, mintWatchTicketCmd } from '$lib/rooms/rooms.remote'; import { setCurrentChannel } from '$lib/rooms/realtime.svelte'; import { markLastRead } from '$lib/rooms/unread.svelte'; import { displayName } from '$lib/rooms/profiles.svelte'; @@ -11,6 +11,8 @@ import { dev } from '$app/environment'; import { setConnectionStatus, resetConnectionStatus } from '$lib/rooms/connection.svelte'; + const WATCH_RECORDS_NSID = 'tools.atmo.chat.message.watchRecords'; + let { data } = $props(); let channel = $derived(data.channels.find((c) => c.key === data.channelKey)); @@ -32,21 +34,26 @@ if (uri === currentUri) return; query?.stop(); currentUri = uri; + + // First connect reuses the ticket minted during SSR (landed on page + // data). Reconnects mint a fresh one via the remote function. + let pending: string | null = data.initialTicket; query = createWatchQuery({ - url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(uri)}&limit=50`, + url: `/xrpc/${WATCH_RECORDS_NSID}?spaceUri=${encodeURIComponent(uri)}&limit=50`, transport: dev ? 'sse' : 'ws', - fetchAuthToken: dev - ? undefined - : async () => { - const res = await fetch('/api/watch-token', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ lxm: 'tools.atmo.chat.message.watchRecords' }) - }); - if (!res.ok) throw new Error(`watch-token mint failed: ${res.status}`); - const d = (await res.json()) as { token: string }; - return d.token; - }, + fetchTicket: async () => { + if (pending) { + const t = pending; + pending = null; + return t; + } + const res = await mintWatchTicketCmd({ + spaceUri: uri, + watchRecordsNsid: WATCH_RECORDS_NSID, + limit: 50 + }); + return res.ticket; + }, compareRecords: (a: WatchRecord, b: WatchRecord) => (a.time_us ?? 0) - (b.time_us ?? 0) }); diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts index 2acc2d9..97059c0 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts @@ -1,11 +1,10 @@ import type { PageServerLoad } from './$types'; import { error } from '@sveltejs/kit'; -import type { Client } from '@atcute/client'; import { listCommunityInvites } from '$lib/rooms/server'; import { buildMembersUri } from '$lib/rooms/uri'; export const load: PageServerLoad = async ({ locals, params, platform }) => { - if (!locals.did || !locals.client) error(401, 'Not authenticated'); + if (!locals.did) error(401, 'Not authenticated'); const communityDid = decodeURIComponent(params.communityDid); const membersUri = buildMembersUri(communityDid); @@ -23,11 +22,7 @@ export const load: PageServerLoad = async ({ locals, params, platform }) => { }> = []; try { const res = await listCommunityInvites( - { - env: platform!.env, - client: locals.client as Client, - did: locals.did as string - }, + { env: platform!.env, did: locals.did as string }, { spaceUri: membersUri, includeRevoked: true } ); invites = res.invites.map((r) => ({ diff --git a/src/core/router/collection.ts b/src/core/router/collection.ts index d6b4968..1918aa8 100644 --- a/src/core/router/collection.ts +++ b/src/core/router/collection.ts @@ -446,9 +446,7 @@ export function registerCollectionRoutes( } const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; - const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid, { - authOverride: config.spaces?.authOverride, - }); + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); if (!auth) { return c.json( { error: "AuthRequired", message: "spaceUri requires a valid service-auth JWT or read-grant invite token" }, @@ -497,17 +495,13 @@ export function registerCollectionRoutes( } } - // Union path: when Authorization is present (or the dev-mode override - // can supply claims without one), verify and fold in records from - // spaces the caller is a member of. + // Union path: when the caller is authenticated, fold in records from + // spaces they're a member of. Anonymous callers just get public results. let spaceUris: string[] | undefined; const hasAuthHeader = !!c.req.header("Authorization"); - const hasOverride = !!config.spaces?.authOverride; - if (spacesCtx && (hasAuthHeader || hasOverride)) { + if (spacesCtx) { const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; - const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid, { - authOverride: config.spaces?.authOverride, - }); + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); if (auth) { const { spaces } = await spacesCtx.adapter.listSpaces({ memberDid: auth.issuer, diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 5636762..8a1b732 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -122,14 +122,12 @@ export function createApp( : null; registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx, communityAdapterForSpaces); - const authOverride = config.spaces?.authOverride; - if (config.community && spacesCtx) { // Community routes reuse the spaces service-auth middleware (same JWT verifier). const authMiddleware = options.community?.authMiddleware ?? options.spaces?.authMiddleware ?? - createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); + createServiceAuthMiddleware(spacesCtx.verifier); registerCommunityRoutes( app, spacesDb, @@ -144,7 +142,7 @@ export function createApp( // space ownership (user-owned → addMember; community-owned → grant). const authMiddleware = options.spaces?.authMiddleware ?? - createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }); + createServiceAuthMiddleware(spacesCtx.verifier); const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; registerInviteRoutes(app, config, spacesCtx.adapter, communityAdapter, { authMiddleware }); } @@ -157,7 +155,7 @@ export function createApp( const authMiddleware = spacesCtx ? options.realtime?.authMiddleware ?? options.spaces?.authMiddleware ?? - createServiceAuthMiddleware(spacesCtx.verifier, { authOverride }) + createServiceAuthMiddleware(spacesCtx.verifier) : null; const communityAdapter = config.community ? new CommunityAdapter(spacesDb) : null; registerRealtimeRoutes(app, config, spacesCtx?.adapter ?? null, communityAdapter, { diff --git a/src/core/spaces/auth.ts b/src/core/spaces/auth.ts index 8a1eb89..b005300 100644 --- a/src/core/spaces/auth.ts +++ b/src/core/spaces/auth.ts @@ -8,6 +8,7 @@ import { } from "@atcute/identity-resolver"; import type { Did, Nsid } from "@atcute/lexicons"; import type { SpacesConfig } from "./types"; +import { readInProcess } from "./in-process"; export { ServiceJwtVerifier }; @@ -41,38 +42,27 @@ export interface ServiceAuthOptions { resolver: DidDocumentResolver; } -/** Hono middleware that verifies the Authorization: Bearer as an atproto - * service-auth token. On success, attaches the decoded claims to c.var.serviceAuth. - * Expected Nsid method is taken from the route pattern (last segment after /xrpc/). +/** Hono middleware that authenticates XRPC requests. Order of precedence: + * 1. In-process marker (same-module calls; see `core/spaces/in-process.ts`) + * 2. Authorization: Bearer as an atproto service-auth token * - * If `authOverride` is provided and returns non-null claims, the JWT check is - * skipped and the override's claims are attached instead — used to paper over - * bsky.social's loopback-client restriction on `getServiceAuth` during dev. - * The override must be gated in the caller (e.g. an env flag) since it - * bypasses cryptographic verification. */ + * On success, attaches the claims to `c.var.serviceAuth`. Expected Nsid is + * taken from the route pattern (last segment after `/xrpc/`). */ export function createServiceAuthMiddleware( - verifier: ServiceJwtVerifier, - options: { - authOverride?: ( - req: Request - ) => Promise | import("./types").AuthOverrideResult | null; - } = {} + verifier: ServiceJwtVerifier ): MiddlewareHandler { return async (c, next) => { const lxm = extractLxmFromPath(c); - if (options.authOverride) { - const override = await options.authOverride(c.req.raw); - if (override) { - c.set("serviceAuth", { - issuer: override.issuer, - audience: override.audience ?? "", - lxm: override.lxm ?? lxm ?? undefined, - clientId: override.clientId, - } satisfies ServiceAuth); - await next(); - return; - } + const inProcess = readInProcess(c.req.raw); + if (inProcess) { + c.set("serviceAuth", { + issuer: inProcess.did, + audience: "", + lxm: lxm ?? undefined, + } satisfies ServiceAuth); + await next(); + return; } const header = c.req.header("Authorization"); @@ -109,32 +99,21 @@ export function requireServiceAuth(c: Context): ServiceAuth { return auth; } -/** Verify a request's Authorization: Bearer token out-of-band (e.g. from a - * route handler that doesn't always require auth). Returns the claims on - * success, or null if missing/invalid. - * - * If `authOverride` is provided and returns claims, the JWT path is skipped - * and those claims are returned — same bypass as the middleware-level auth. */ +/** Out-of-band auth check for handlers that don't always require auth. + * Returns claims on success, or null if no valid credentials are present. + * Order of precedence: in-process marker → service-auth JWT. */ export async function verifyServiceAuthRequest( verifier: ServiceJwtVerifier, request: Request, - lxm?: Nsid | null, - options: { - authOverride?: ( - req: Request - ) => Promise | import("./types").AuthOverrideResult | null; - } = {} + lxm?: Nsid | null ): Promise { - if (options.authOverride) { - const override = await options.authOverride(request); - if (override) { - return { - issuer: override.issuer, - audience: override.audience ?? "", - lxm: override.lxm ?? lxm ?? undefined, - clientId: override.clientId, - }; - } + const inProcess = readInProcess(request); + if (inProcess) { + return { + issuer: inProcess.did, + audience: "", + lxm: lxm ?? undefined, + }; } const header = request.headers.get("Authorization"); diff --git a/src/core/spaces/in-process.ts b/src/core/spaces/in-process.ts new file mode 100644 index 0000000..ec1d7a1 --- /dev/null +++ b/src/core/spaces/in-process.ts @@ -0,0 +1,34 @@ +/** In-process auth marker. + * + * For same-module callers (e.g. a SvelteKit worker that imports contrail and + * dispatches requests directly to the handler), service-auth JWTs are pure + * overhead: no network boundary is crossed, so there's nothing for the JWT to + * protect against. Instead, the caller tags the `Request` with a principal via + * a module-private WeakMap, and the auth middleware reads it back. + * + * Security note: this is unforgeable from outside the module because + * - WeakMap keys are `Request` object identities, not serialized data; + * - no HTTP request crossing a network boundary can reach into this map; + * - exploiting it requires code execution inside the same isolate, at + * which point auth is already game over. + * + * This is the strongest auth adapter contrail offers — it has no secret to + * leak. */ + +export interface InProcessPrincipal { + did: string; +} + +const PRINCIPALS = new WeakMap(); + +/** Tag a Request with an in-process principal. The returned Request is the + * same reference; the return value is for ergonomics. */ +export function markInProcess(req: Request, did: string): Request { + PRINCIPALS.set(req, { did }); + return req; +} + +/** Read the in-process principal for a Request, or null if unmarked. */ +export function readInProcess(req: Request): InProcessPrincipal | null { + return PRINCIPALS.get(req) ?? null; +} diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index b955150..e3fa1c7 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -45,11 +45,7 @@ export function registerSpacesRoutes( const adapter = options.adapter ?? ctx?.adapter ?? new HostedAdapter(db, config); const verifier = ctx?.verifier ?? buildVerifier(spacesConfig); - const auth = - options.authMiddleware ?? - createServiceAuthMiddleware(verifier, { - authOverride: spacesConfig.authOverride, - }); + const auth = options.authMiddleware ?? createServiceAuthMiddleware(verifier); /** Read-route auth: skip the JWT middleware when an `?inviteToken=` is * present so anonymous bearer reads don't 401 before the route handler can diff --git a/src/core/spaces/types.ts b/src/core/spaces/types.ts index 8ba8374..397e6a8 100644 --- a/src/core/spaces/types.ts +++ b/src/core/spaces/types.ts @@ -37,24 +37,6 @@ export interface SpacesConfig { resolver?: DidDocumentResolver; /** Blob-upload backend. When omitted, blob XRPCs are not exposed. */ blobs?: SpacesBlobsConfig; - /** Dev-only auth bypass. Runs BEFORE JWT verification. If it returns - * non-null claims, those are used as the authenticated caller and the - * JWT check is skipped. Intended for local development where bsky.social - * rejects `getServiceAuth` for loopback OAuth clients; pair with a - * signed-cookie session check so only your own browser can trigger it. - * Never set in production. */ - authOverride?: ( - req: Request - ) => Promise | AuthOverrideResult | null; -} - -/** Claims returned by an `authOverride`. Matches the ServiceAuth shape the - * normal JWT path attaches to the request. */ -export interface AuthOverrideResult { - issuer: string; - audience?: string; - lxm?: string; - clientId?: string; } export interface SpaceRow { diff --git a/src/index.ts b/src/index.ts index dbd672f..feff577 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,7 +35,6 @@ export type { SpacesConfig, AppPolicy, AppPolicyMode, - AuthOverrideResult, SpaceRow, SpaceMemberRow, StoredRecord, diff --git a/src/server.ts b/src/server.ts index ae4f47f..3e1bcf8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,7 @@ +import { Client } from "@atcute/client"; import type { Contrail } from "./contrail"; import type { Database } from "./core/types"; +import { markInProcess } from "./core/spaces/in-process"; /** * Create an HTTP handler from a Contrail instance. @@ -27,3 +29,33 @@ export function createHandler( return cached(request); }; } + +/** + * Fully typed `@atcute/client` Client that routes XRPC calls through a + * contrail handler in-process — no HTTP roundtrip, no JWT minting. + * + * Pass `did` to act as that user; omit for anonymous calls (public endpoints + * only). Authentication is via the in-process WeakMap marker, which is + * unforgeable across network boundaries. + * + * // Same-process on Cloudflare Workers (per-request DB): + * const client = createServerClient( + * (req) => contrail.handler({ db: env.DB })(req), + * session.did, + * ); + * await client.post('tools.atmo.chat.space.putRecord', { input: { ... } }); + */ +export function createServerClient( + handle: (req: Request) => Promise, + did?: string +): Client { + return new Client({ + handler: async (pathname, init) => { + const req = new Request(new URL(pathname, "http://localhost"), init); + if (did !== undefined) markInProcess(req, did); + return handle(req); + }, + }); +} + +export { markInProcess }; diff --git a/src/sync/index.ts b/src/sync/index.ts index 2a28a37..4555e30 100644 --- a/src/sync/index.ts +++ b/src/sync/index.ts @@ -33,18 +33,15 @@ export interface WatchStoreOptions { /** Transport. Default: 'sse'. Use 'ws' on Cloudflare for DO-terminated * long-lived subscriptions that hibernate while idle. */ transport?: "sse" | "ws"; - /** Optional: provide a ticket string (for `?ticket=...`-style auth). - * When omitted, the connection is opened without an explicit token — - * rely on cookies / bearer tokens your fetch policy sends automatically. */ + /** Mint a watch-scoped ticket to auth the connection. Called once per + * connect attempt; the returned ticket is sent as `?ticket=...` on the + * SSE connection or on the `mode=ws` handshake fetch. Omit for public + * (no-auth) endpoints. + * + * For atproto-based services, tickets are typically minted server-side + * via `.realtime.ticket` (or via an app-specific route that runs + * the `mode=ws` handshake in-process and returns the signed ticket). */ fetchTicket?: () => Promise; - /** Optional: mint a short-lived bearer token (typically an atproto - * service-auth JWT scoped to `.watchRecords`). Called once - * per connect attempt; returned token is sent as `Authorization: Bearer` - * on the handshake fetch (`mode=ws`) or on the SSE connection via a - * `?auth=` query param the server doesn't use — browsers can't set - * headers on EventSource, so for SSE the app should cookie-auth or - * fall back to `fetchTicket`. */ - fetchAuthToken?: () => Promise; /** Custom compare. Default: sort by `time_us` descending (newest first), * tie-breaking by rkey. */ compareRecords?: (a: WatchRecord, b: WatchRecord) => number; @@ -312,12 +309,11 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { }; const openWs = async () => { - // Step 1: snapshot + handshake. Authenticated by Bearer token from - // `fetchAuthToken` (an atproto service-auth JWT — browsers get this - // from a same-origin helper that uses the OAuth session). The server + // Step 1: snapshot + handshake. Authenticated by the watch-scoped + // ticket from `fetchTicket`, passed as `?ticket=...`. The server // returns a ticket bound to (did, spaceUri, querySpec); we pass that // back embedded in the wsUrl on the WS upgrade so the WS itself - // doesn't need a JWT (EventSource/WebSocket can't set headers). + // doesn't need any other auth. let snapshotUrl = options.url; const sep = snapshotUrl.includes("?") ? "&" : "?"; snapshotUrl += `${sep}mode=ws`; @@ -325,12 +321,7 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { const ticket = await options.fetchTicket(); snapshotUrl += `&ticket=${encodeURIComponent(ticket)}`; } - const headers: Record = { accept: "application/json" }; - if (options.fetchAuthToken) { - const token = await options.fetchAuthToken(); - headers.authorization = `Bearer ${token}`; - } - const res = await fetch(snapshotUrl, { headers }); + const res = await fetch(snapshotUrl, { headers: { accept: "application/json" } }); if (!res.ok) throw new Error(`snapshot fetch failed (${res.status})`); const handshake = (await res.json()) as { transport: "ws"; -- 2.51.2 From 0fd0d7cf3892cf79c4ab5da5ebc32cbcc2e478ac Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:02:06 +0200 Subject: [PATCH 13/18] sync stuff --- .../src/lib/rooms/rooms.remote.ts | 6 +- .../sveltekit-group-chat/src/lib/rooms/uri.ts | 16 +- .../src/lib/rooms/watch.svelte.ts | 320 +++++++++++++++--- .../[channelKey]/+page.server.ts | 22 +- .../[communityDid]/[channelKey]/+page.svelte | 134 ++++---- package.json | 4 + src/sync/cache-idb.ts | 91 +++++ src/sync/index.ts | 176 +++++++++- todo/cross-space-watch-records.md | 120 +++++++ todo/realtime-do-dev-wiring.md | 104 ++++++ tsup.config.ts | 1 + 11 files changed, 840 insertions(+), 154 deletions(-) create mode 100644 src/sync/cache-idb.ts create mode 100644 todo/cross-space-watch-records.md create mode 100644 todo/realtime-do-dev-wiring.md diff --git a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts index 23cfefb..2f8402e 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts @@ -162,7 +162,10 @@ export const createChannel = command(CreateChannelInput, async (input) => { const PostMessageInput = v.object({ spaceUri: v.pipe(v.string(), v.minLength(1)), text: v.pipe(v.string(), v.minLength(1), v.maxLength(4000)), - replyTo: v.optional(v.pipe(v.string(), v.minLength(1))) + replyTo: v.optional(v.pipe(v.string(), v.minLength(1))), + /** Optional client-generated rkey — lets the browser match an optimistic + * record to the stream's `record.created` event by identity. */ + rkey: v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(64))) }); export const postMessage = command(PostMessageInput, async (input) => { @@ -170,6 +173,7 @@ export const postMessage = command(PostMessageInput, async (input) => { const res = await spacePutRecord(ctx, { spaceUri: input.spaceUri, collection: 'tools.atmo.chat.message', + ...(input.rkey ? { rkey: input.rkey } : {}), record: { text: input.text, createdAt: new Date().toISOString(), diff --git a/examples/sveltekit-group-chat/src/lib/rooms/uri.ts b/examples/sveltekit-group-chat/src/lib/rooms/uri.ts index 4a263cf..27a6992 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/uri.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/uri.ts @@ -1,16 +1,22 @@ -/** Helpers for assembling space URIs in the `tools.atmo.chat` namespace. */ +/** Helpers for assembling space URIs in the `tools.atmo.chat` namespace. + * + * The URIs are returned as `ResourceUri` (branded) so they flow into + * lexicon-typed XRPC params without per-call casts. We apply the brand + * once here — structurally these are just `at://…` strings. */ + +import type { ResourceUri } from '@atcute/lexicons'; const SPACE_TYPE = 'tools.atmo.chat.space'; -export function buildSpaceUri(communityDid: string, key: string): string { - return `at://${communityDid}/${SPACE_TYPE}/${key}`; +export function buildSpaceUri(communityDid: string, key: string): ResourceUri { + return `at://${communityDid}/${SPACE_TYPE}/${key}` as ResourceUri; } -export function buildMembersUri(communityDid: string): string { +export function buildMembersUri(communityDid: string): ResourceUri { return buildSpaceUri(communityDid, 'members'); } -export function buildAdminUri(communityDid: string): string { +export function buildAdminUri(communityDid: string): ResourceUri { return buildSpaceUri(communityDid, '$admin'); } diff --git a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts index 28693cd..34057ea 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts @@ -1,56 +1,286 @@ -/** Svelte 5 binding over `@atmo-dev/contrail/sync`. Turns the framework-agnostic - * subscribable store into a reactive `$state` object that Svelte effects can - * observe in the usual way. +/** Svelte 5 binding over `@atmo-dev/contrail/sync`. * * Usage: - * const query = createWatchQuery({ - * url: `/xrpc/tools.atmo.chat.message.watchRecords?spaceUri=${encodeURIComponent(spaceUri)}`, - * }); - * // template: {#each query.records as r (r.uri)} ... - * // teardown: $effect(() => () => query.stop()); + * const messagesQuery = $derived( + * createWatchQuery({ + * endpoint: 'tools.atmo.chat.message', + * params: { spaceUri, limit: 50 } + * }) + * ); + * // template: {#each messagesQuery.records as r (r.rkey)} {r.record.text} + * + * Records and params are typed via the app's generated `XRPCQueries` / + * `Records` ambient declarations (from `src/lexicon-types/`). Reading + * `.records` auto-subscribes; when no component is reading anymore + * (e.g. a $derived query instance is replaced on prop change), the + * underlying stream is torn down via `createSubscriber`. + * + * Authentication: tickets are minted on demand. By default the wrapper + * calls the `mintWatchTicketCmd` remote function; override with + * `setContrailTicketMinter(fn)` if your app uses a different route. */ -import { createWatchStore, type WatchRecord, type WatchStoreOptions, type WatchStoreStatus } from '@atmo-dev/contrail/sync'; +import { createSubscriber } from 'svelte/reactivity'; +import { browser } from '$app/environment'; +import { + createWatchStore, + type WatchCache, + type WatchRecord, + type WatchStore, + type WatchStoreStatus +} from '@atmo-dev/contrail/sync'; +import { createIndexedDBCache } from '@atmo-dev/contrail/sync/cache-idb'; +import type { InferInput } from '@atcute/lexicons'; +import type { BaseSchema } from '@atcute/lexicons/validations'; +import type { Records, XRPCQueries } from '@atcute/lexicons/ambient'; +import { dev } from '$app/environment'; +import { mintWatchTicketCmd } from './rooms.remote'; + +// --------------------------------------------------------------------------- +// Type machinery — derives params + record types from the app's lexicons. +// --------------------------------------------------------------------------- + +type WatchRecordsOf = `${K}.watchRecords`; + +/** Params type for `.watchRecords`, from ambient XRPCQueries. */ +type WatchParamsOf = + WatchRecordsOf extends keyof XRPCQueries + ? XRPCQueries[WatchRecordsOf] extends { params: infer P } + ? P extends BaseSchema + ? InferInput

+ : Record + : Record + : Record; -export interface WatchQuery { - readonly records: ReadonlyArray; - readonly status: WatchStoreStatus; - readonly error: Error | null; - stop(): void; +/** Record value-shape for a collection, from ambient Records. */ +export type RecordShapeOf = K extends keyof Records + ? Records[K] extends BaseSchema + ? InferInput + : Record + : Record; + +/** WatchRecord with a typed `record` payload. Mirrors WatchRecord's explicit + * fields but without the `[k: string]: unknown` catchall (which would + * poison property access under `Omit`). */ +export interface TypedWatchRecord { + uri: string; + did: string; + rkey: string; + collection: string; + record: R; + time_us?: number; + indexed_at?: number; + cid?: string | null; + _space?: string; + /** Present on optimistic entries added via `addOptimistic`. */ + optimistic?: 'pending' | 'failed'; + /** Error attached via `markFailed`. */ + optimisticError?: Error; } -export function createWatchQuery(options: WatchStoreOptions): WatchQuery { - const state = $state<{ - records: ReadonlyArray; - status: WatchStoreStatus; - error: Error | null; - }>({ - records: [], - status: 'idle', - error: null - }); +// --------------------------------------------------------------------------- +// Configurable ticket minter — called when the wrapper needs a fresh ticket. +// Default: uses `mintWatchTicketCmd`. Override via `setContrailTicketMinter`. +// --------------------------------------------------------------------------- + +export interface TicketMintContext { + endpoint: string; + params: Record; +} - const store = createWatchStore(options); - const unsub = store.subscribe((s) => { - state.records = s.records; - state.status = s.status; - state.error = s.error; +export type TicketMinter = (ctx: TicketMintContext) => Promise; + +let ticketMinter: TicketMinter | null = async ({ endpoint, params }) => { + const res = await mintWatchTicketCmd({ + watchRecordsNsid: `${endpoint}.watchRecords`, + spaceUri: String(params.spaceUri ?? ''), + limit: typeof params.limit === 'number' ? params.limit : 50 }); - store.start(); - - return { - get records() { - return state.records; - }, - get status() { - return state.status; - }, - get error() { - return state.error; - }, - stop() { - unsub(); - store.stop(); + return res.ticket; +}; + +export function setContrailTicketMinter(fn: TicketMinter | null): void { + ticketMinter = fn; +} + +// --------------------------------------------------------------------------- +// Configurable cache — persists last-seen records for instant first paint. +// Default: IndexedDB in the browser, no-op during SSR. Override via +// `setContrailCache(cache)` or opt out with `setContrailCache(null)`. +// --------------------------------------------------------------------------- + +let cache: WatchCache | null = browser ? createIndexedDBCache() : null; + +export function setContrailCache(next: WatchCache | null): void { + cache = next; +} + +// --------------------------------------------------------------------------- +// createWatchQuery +// --------------------------------------------------------------------------- + +export interface WatchQueryOptions { + /** Collection NSID. The library appends `.watchRecords` to build the URL. */ + endpoint: K; + /** Query params for the watchRecords endpoint. Typed per-endpoint. */ + params?: WatchParamsOf; + /** Optional pre-minted ticket (typically from SSR `+page.server.ts`). + * Used once on the first connect; reconnects fall through to the + * configured ticket minter. */ + initialTicket?: string; + /** Per-query override for ticket minting. If omitted, uses the minter + * configured via `setContrailTicketMinter` (or the default). */ + mintTicket?: string | (() => Promise); + /** Transport. Default: 'ws' in prod, 'sse' in dev (Vite dev's DO wiring + * is limited — see `todo/realtime-do-dev-wiring.md`). */ + transport?: 'sse' | 'ws'; + /** Sort comparator. Default: newest first by `time_us`. */ + compare?: ( + a: TypedWatchRecord>, + b: TypedWatchRecord> + ) => number; + /** Auto-reconnect on error. Default: true. */ + reconnect?: boolean; + /** Per-query cache override. If omitted, uses the wrapper's configured + * cache (see `setContrailCache`). Pass `null` to disable caching for + * this query specifically. */ + cache?: WatchCache | null; + /** Custom cache key. Defaults to the built watchRecords URL. */ + cacheKey?: string; + /** Max records retained in cache. Default: 200. */ + cacheMaxRecords?: number; +} + +export class WatchQuery { + #records = $state>[]>([]); + #status = $state('idle'); + #error = $state(null); + #subscribe: () => void; + #store: WatchStore; + #collection: K; + + constructor(opts: WatchQueryOptions) { + const endpoint = opts.endpoint; + const params = (opts.params ?? {}) as Record; + const url = buildWatchUrl(endpoint, params); + this.#collection = endpoint; + + // Ticket source resolution: initialTicket (one-shot) → per-call mintTicket → + // module-level default. Undefined if none available (public endpoints). + let pendingInitial = opts.initialTicket; + const hasSource = !!(opts.initialTicket || opts.mintTicket || ticketMinter); + + const mintTicket: string | (() => Promise) | undefined = hasSource + ? async () => { + if (pendingInitial) { + const t = pendingInitial; + pendingInitial = undefined; + return t; + } + if (typeof opts.mintTicket === 'string') return opts.mintTicket; + if (typeof opts.mintTicket === 'function') return opts.mintTicket(); + if (ticketMinter) return ticketMinter({ endpoint, params }); + throw new Error('createWatchQuery: no ticket source available'); + } + : undefined; + + // Store is created eagerly so optimistic mutations (add/markFailed/remove) + // can target it from non-reactive contexts (e.g. a submit handler) even + // before any template reader has activated the subscriber. The actual + // network connection only opens when `createSubscriber` fires `start()` + // on first read, and closes when no readers remain. + // Cache: explicit override (including `null`) wins over the module-level + // default. `opts.cache === undefined` means "use whatever's configured." + const effectiveCache = opts.cache === undefined ? cache : opts.cache; + + this.#store = createWatchStore({ + url, + transport: opts.transport ?? (dev ? 'sse' : 'ws'), + reconnect: opts.reconnect, + mintTicket, + compareRecords: opts.compare as + | ((a: WatchRecord, b: WatchRecord) => number) + | undefined, + cache: effectiveCache ?? undefined, + cacheKey: opts.cacheKey, + cacheMaxRecords: opts.cacheMaxRecords + }); + + this.#subscribe = createSubscriber((update) => { + const unsub = this.#store.subscribe((s) => { + this.#records = s.records as unknown as readonly TypedWatchRecord>[]; + this.#status = s.status; + this.#error = s.error; + update(); + }); + this.#store.start(); + return () => { + unsub(); + this.#store.stop(); + }; + }); + } + + get records(): readonly TypedWatchRecord>[] { + this.#subscribe(); + return this.#records; + } + get status(): WatchStoreStatus { + this.#subscribe(); + return this.#status; + } + get error(): Error | null { + this.#subscribe(); + return this.#error; + } + + /** Insert an optimistic record into the query. It shows in `.records` + * immediately with `optimistic: 'pending'` and is auto-dropped when a + * server-confirmed record with the same rkey arrives via the stream. */ + addOptimistic(input: { + rkey: string; + did: string; + record: RecordShapeOf; + time_us?: number; + }): void { + this.#store.addOptimistic({ + rkey: input.rkey, + did: input.did, + collection: this.#collection, + record: input.record as Record, + time_us: input.time_us + }); + } + + /** Flip an optimistic entry's state to `'failed'` and attach an error. */ + markFailed(rkey: string, err: Error): void { + this.#store.markFailed(rkey, err); + } + + /** Remove an optimistic entry (explicit rollback). */ + removeOptimistic(rkey: string): void { + this.#store.removeOptimistic(rkey); + } +} + +export function createWatchQuery( + opts: WatchQueryOptions +): WatchQuery { + return new WatchQuery(opts); +} + +// --------------------------------------------------------------------------- + +function buildWatchUrl(endpoint: string, params: Record): string { + const qs = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v == null) continue; + if (Array.isArray(v)) { + for (const item of v) qs.append(k, String(item)); + } else { + qs.set(k, String(v)); } - }; + } + const query = qs.toString(); + return `/xrpc/${endpoint}.watchRecords${query ? `?${query}` : ''}`; } diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts index e05db66..ed88f76 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts @@ -1,33 +1,15 @@ import type { PageServerLoad } from './$types'; import { error } from '@sveltejs/kit'; import { buildSpaceUri } from '$lib/rooms/uri'; -import { mintWatchTicket } from '$lib/rooms/server'; -const WATCH_RECORDS_NSID = 'tools.atmo.chat.message.watchRecords'; - -export const load: PageServerLoad = async ({ locals, params, platform }) => { +export const load: PageServerLoad = async ({ locals, params }) => { if (!locals.did) error(401, 'Not authenticated'); const communityDid = decodeURIComponent(params.communityDid); const spaceUri = buildSpaceUri(communityDid, params.channelKey); - // Mint the first watch-scoped ticket alongside the page data so the browser - // can open the stream on first paint without a second roundtrip. Reconnects - // call the `mintWatchTicket` remote function. - let initialTicket: string | null = null; - try { - const t = await mintWatchTicket( - { env: platform!.env, did: locals.did as string }, - { watchRecordsNsid: WATCH_RECORDS_NSID, spaceUri, limit: 50 } - ); - initialTicket = t.ticket; - } catch { - // Non-fatal: the browser will mint on demand via the remote function. - } - return { spaceUri, channelKey: params.channelKey, - myDid: locals.did, - initialTicket + myDid: locals.did }; }; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte index 616dafd..447da11 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte @@ -2,71 +2,30 @@ import { tick } from 'svelte'; import { Button, Input, Navbar } from '@foxui/core'; import { RelativeTime } from '@foxui/time'; - import { postMessage, mintWatchTicketCmd } from '$lib/rooms/rooms.remote'; + import { postMessage } from '$lib/rooms/rooms.remote'; import { setCurrentChannel } from '$lib/rooms/realtime.svelte'; import { markLastRead } from '$lib/rooms/unread.svelte'; import { displayName } from '$lib/rooms/profiles.svelte'; import { createWatchQuery } from '$lib/rooms/watch.svelte'; - import type { WatchRecord } from '@atmo-dev/contrail/sync'; - import { dev } from '$app/environment'; import { setConnectionStatus, resetConnectionStatus } from '$lib/rooms/connection.svelte'; - - const WATCH_RECORDS_NSID = 'tools.atmo.chat.message.watchRecords'; + import { nextTid } from '@atmo-dev/contrail'; let { data } = $props(); let channel = $derived(data.channels.find((c) => c.key === data.channelKey)); let channelName = $derived(channel?.name ?? data.channelKey); - // Live message feed via contrail's watchRecords subscription. The engine - // handles the snapshot + live merge; we just render its `records` array. - // Sorted newest-first by default — flip to oldest-first for chat. - // - // The query is keyed strictly on spaceUri — recreating it on every - // `data` identity change would tear down and rebuild the subscription on - // unrelated page-data invalidations (e.g. channel list refresh), flashing - // "Loading…" each time. Using a keyed effect here avoids that. - let query = $state | null>(null); - let currentUri: string | null = null; - - $effect(() => { - const uri = data.spaceUri; - if (uri === currentUri) return; - query?.stop(); - currentUri = uri; - - // First connect reuses the ticket minted during SSR (landed on page - // data). Reconnects mint a fresh one via the remote function. - let pending: string | null = data.initialTicket; - query = createWatchQuery({ - url: `/xrpc/${WATCH_RECORDS_NSID}?spaceUri=${encodeURIComponent(uri)}&limit=50`, - transport: dev ? 'sse' : 'ws', - fetchTicket: async () => { - if (pending) { - const t = pending; - pending = null; - return t; - } - const res = await mintWatchTicketCmd({ - spaceUri: uri, - watchRecordsNsid: WATCH_RECORDS_NSID, - limit: 50 - }); - return res.ticket; - }, - compareRecords: (a: WatchRecord, b: WatchRecord) => - (a.time_us ?? 0) - (b.time_us ?? 0) - }); - }); - - // Final teardown on component unmount. - $effect(() => { - return () => { - query?.stop(); - query = null; - currentUri = null; - }; - }); + // Live message feed. `$derived` recreates the query when spaceUri changes; + // the old instance is auto-torn down via `createSubscriber` once no + // component reads it. Initial ticket is pre-minted during SSR to save one + // roundtrip; reconnects call the configured ticket minter transparently. + let messagesQuery = $derived( + createWatchQuery({ + endpoint: 'tools.atmo.chat.message', + params: { spaceUri: data.spaceUri, limit: 50 }, + compare: (a, b) => (a.time_us ?? 0) - (b.time_us ?? 0) + }) + ); // Unread / current-channel bookkeeping. $effect(() => { @@ -78,22 +37,21 @@ // Mirror the query's connection status into the shared store so the // layout's navbar can render a dot for it. $effect(() => { - if (query) setConnectionStatus(query.status); + setConnectionStatus(messagesQuery.status); return () => resetConnectionStatus(); }); - // Project records to the shape the existing template expects. let messages = $derived( - (query?.records ?? []).map((r) => { - const rec = r.record as { text?: string; createdAt?: string; replyTo?: string }; - return { - rkey: r.rkey, - authorDid: r.did, - text: rec.text ?? '', - createdAt: rec.createdAt ?? '', - replyTo: rec.replyTo - }; - }) + messagesQuery.records.map((r) => ({ + rkey: r.rkey, + authorDid: r.did, + text: r.record.text ?? '', + createdAt: r.record.createdAt ?? '', + replyTo: r.record.replyTo, + pending: r.optimistic === 'pending', + failed: r.optimistic === 'failed', + error: r.optimisticError + })) ); let scrollEl: HTMLDivElement | null = $state(null); @@ -114,11 +72,26 @@ if (!t) return; sending = true; sendErr = null; + // Client-generated rkey lets the optimistic entry reconcile with the + // stream's record.created event by identity. + const rkey = nextTid(); + messagesQuery.addOptimistic({ + rkey, + did: data.myDid, + record: { + $type: 'tools.atmo.chat.message', + text: t, + createdAt: new Date().toISOString() + } + }); + text = ''; try { - await postMessage({ spaceUri: data.spaceUri, text: t }); - text = ''; + await postMessage({ spaceUri: data.spaceUri, rkey, text: t }); + // Success: leave the optimistic entry — the stream will replace it. } catch (err) { - sendErr = err instanceof Error ? err.message : String(err); + const e = err instanceof Error ? err : new Error(String(err)); + messagesQuery.markFailed(rkey, e); + sendErr = e.message; } finally { sending = false; } @@ -132,8 +105,20 @@

+ {#if messages.length > 0 && (messagesQuery.status === 'connecting' || messagesQuery.status === 'snapshot')} +
+ + + + + updating +
+ {/if}
- {#if messages.length === 0 && (query?.status === 'connecting' || query?.status === 'snapshot' || query?.status === 'idle')} + {#if messages.length === 0 && (messagesQuery.status === 'connecting' || messagesQuery.status === 'snapshot' || messagesQuery.status === 'idle')}
Loading…
{:else if messages.length === 0}
No messages yet. Say hi.
@@ -147,11 +132,18 @@ {displayName(m.authorDid)} · + {#if m.pending} + · sending… + {:else if m.failed} + · failed{m.error ? ` (${m.error.message})` : ''} + {/if}
{m.text}
diff --git a/package.json b/package.json index 9a5d155..2b915bd 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,10 @@ "types": "./dist/sync/index.d.ts", "import": "./dist/sync/index.js" }, + "./sync/cache-idb": { + "types": "./dist/sync/cache-idb.d.ts", + "import": "./dist/sync/cache-idb.js" + }, "./lexicon-templates/*.json": "./lexicon-templates/*.json" }, "repository": { diff --git a/src/sync/cache-idb.ts b/src/sync/cache-idb.ts new file mode 100644 index 0000000..2b2f474 --- /dev/null +++ b/src/sync/cache-idb.ts @@ -0,0 +1,91 @@ +/// +/** IndexedDB-backed implementation of `WatchCache`. Browser-only. + * + * Single object store keyed by the watch query's `cacheKey` (defaults to + * the watch URL). Values are `{records, updatedAt}`. Persists across page + * reloads; cleared by the user clearing site data. */ + +import type { WatchCache, WatchRecord } from "./index.js"; + +interface StoredEntry { + records: WatchRecord[]; + updatedAt: number; +} + +export interface IndexedDBCacheOptions { + /** Database name. Default: 'contrail-watch-cache'. Bump this (or delete + * via DevTools) if you need to invalidate all cached watch data. */ + dbName?: string; + /** Object store name within the DB. Default: 'watches'. */ + storeName?: string; + /** IDB schema version. Default: 1. Only bump when changing + * storeName / schema. */ + version?: number; +} + +export function createIndexedDBCache( + options: IndexedDBCacheOptions = {} +): WatchCache { + const dbName = options.dbName ?? "contrail-watch-cache"; + const storeName = options.storeName ?? "watches"; + const version = options.version ?? 1; + + let dbPromise: Promise | null = null; + const openDb = (): Promise => { + if (dbPromise) return dbPromise; + dbPromise = new Promise((resolve, reject) => { + if (typeof indexedDB === "undefined") { + reject(new Error("IndexedDB not available in this environment")); + return; + } + const req = indexedDB.open(dbName, version); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + req.onblocked = () => + reject(new Error("IDB open blocked (another tab holds an older version)")); + }); + return dbPromise; + }; + + return { + async read(key) { + try { + const db = await openDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(storeName, "readonly"); + const store = tx.objectStore(storeName); + const req = store.get(key); + req.onsuccess = () => { + const entry = req.result as StoredEntry | undefined; + resolve(entry?.records ?? null); + }; + req.onerror = () => reject(req.error); + }); + } catch { + return null; + } + }, + async write(key, records) { + try { + const db = await openDb(); + await new Promise((resolve, reject) => { + const tx = db.transaction(storeName, "readwrite"); + const store = tx.objectStore(storeName); + const entry: StoredEntry = { records, updatedAt: Date.now() }; + store.put(entry, key); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error ?? new Error("tx aborted")); + }); + } catch { + // Intentional: caching is best-effort; swallow. + } + }, + }; +} diff --git a/src/sync/index.ts b/src/sync/index.ts index 4555e30..4161e3d 100644 --- a/src/sync/index.ts +++ b/src/sync/index.ts @@ -23,6 +23,12 @@ export interface WatchRecord { cid?: string | null; /** Set when the record originates from a per-space table. */ _space?: string; + /** Present on optimistic entries added via `addOptimistic` — not set by + * records arriving from the stream. Auto-dropped when a real record + * with the same rkey arrives via `record.created`. */ + optimistic?: "pending" | "failed"; + /** Error attached via `markFailed` after a failed mutation. */ + optimisticError?: Error; /** Additional hydrated relations / references populated server-side. */ [k: string]: unknown; } @@ -33,20 +39,34 @@ export interface WatchStoreOptions { /** Transport. Default: 'sse'. Use 'ws' on Cloudflare for DO-terminated * long-lived subscriptions that hibernate while idle. */ transport?: "sse" | "ws"; - /** Mint a watch-scoped ticket to auth the connection. Called once per - * connect attempt; the returned ticket is sent as `?ticket=...` on the - * SSE connection or on the `mode=ws` handshake fetch. Omit for public - * (no-auth) endpoints. + /** Watch-scoped ticket auth for the connection. Sent as `?ticket=...` + * on the SSE connection or on the `mode=ws` handshake fetch. + * + * - `string` — used as-is on every connect (good for one-shot SSR-minted + * tokens; reconnects after expiry will surface a 401). + * - `() => Promise` — called once per connect attempt; a fresh + * ticket is minted for every (re)connect. + * - omitted — public (no-auth) endpoints. * * For atproto-based services, tickets are typically minted server-side * via `.realtime.ticket` (or via an app-specific route that runs * the `mode=ws` handshake in-process and returns the signed ticket). */ - fetchTicket?: () => Promise; + mintTicket?: string | (() => Promise); /** Custom compare. Default: sort by `time_us` descending (newest first), * tie-breaking by rkey. */ compareRecords?: (a: WatchRecord, b: WatchRecord) => number; /** Reconnect on error with backoff. Default: true, 1s → 30s exponential. */ reconnect?: boolean; + /** Optional persistent cache. When provided, the store loads cached + * records at `start()` for instant first paint, then reconciles against + * the live snapshot as usual. Writes are debounced and exclude + * optimistic entries. */ + cache?: WatchCache; + /** Key for this query in the cache. Defaults to `url`. */ + cacheKey?: string; + /** Max records retained in the cache per key. Excess records (by sort + * order) are dropped on write. Default: 200. */ + cacheMaxRecords?: number; /** Optional logger for debug output. */ logger?: { log?: (...args: unknown[]) => void; @@ -55,6 +75,15 @@ export interface WatchStoreOptions { }; } +/** Adapter interface for persisting the last-seen records of a watch query. + * Implementations can back this with IndexedDB, localStorage, fs, etc. + * Errors should be caught internally — caching is a best-effort optimization + * and must never break the live stream. */ +export interface WatchCache { + read(key: string): Promise; + write(key: string, records: WatchRecord[]): Promise; +} + export type WatchStoreStatus = | "idle" | "connecting" @@ -73,6 +102,24 @@ export interface WatchStore { start(): void; /** Close the connection and clear the store. */ stop(): void; + + /** Insert an optimistic record immediately, before the server confirms. + * The entry is merged into `.records` with `optimistic: 'pending'`. When + * a real record arrives via the stream with the same `rkey`, the + * optimistic entry is dropped automatically. */ + addOptimistic(input: { + rkey: string; + did: string; + collection?: string; + record: Record; + time_us?: number; + uri?: string; + }): void; + /** Flip an optimistic entry to `optimistic: 'failed'` and attach an error. + * No-op if no optimistic entry matches. */ + markFailed(rkey: string, err: Error): void; + /** Remove an optimistic entry (explicit rollback). */ + removeOptimistic(rkey: string): void; } export interface WatchStoreState { @@ -95,13 +142,45 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { const reconnect = options.reconnect !== false; const transport = options.transport ?? "sse"; const log = options.logger ?? {}; + const cache = options.cache ?? null; + const cacheKey = options.cacheKey ?? options.url; + const cacheMax = options.cacheMaxRecords ?? 200; const byKey = new Map(); + /** Optimistic entries keyed by rkey. Merged into `.records`; dropped when + * a real record with the same rkey arrives via `record.created`. */ + const optimisticByRkey = new Map(); let sorted: WatchRecord[] = []; let status: WatchStoreStatus = "idle"; let error: Error | null = null; const listeners = new Set<(state: WatchStoreState) => void>(); + // Debounced cache writer. Schedules one write after the current burst of + // state changes settles, to avoid writing on every incoming event. + let cacheWriteTimer: ReturnType | null = null; + const scheduleCacheWrite = () => { + if (!cache) return; + if (cacheWriteTimer) return; + cacheWriteTimer = setTimeout(() => { + cacheWriteTimer = null; + void flushCache(); + }, 200); + }; + const flushCache = async () => { + if (!cache) return; + // Snapshot the current server-confirmed records (exclude optimistic) + // and trim to cacheMax. + const confirmed: WatchRecord[] = []; + for (const r of byKey.values()) confirmed.push(r); + confirmed.sort(compare); + const trimmed = confirmed.slice(0, cacheMax); + try { + await cache.write(cacheKey, trimmed); + } catch (err) { + log.warn?.("cache write failed", err); + } + }; + // Per-snapshot reconcile: during a snapshot we track which keys were // included, and on snapshot.end we evict any stale keys the previous // snapshot had but this one didn't. Keeps data visible across reconnects @@ -119,10 +198,14 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { const notify = () => { const s = stateSnapshot(); for (const l of listeners) l(s); + scheduleCacheWrite(); }; const resort = () => { - sorted = Array.from(byKey.values()).sort(compare); + const merged: WatchRecord[] = []; + for (const r of byKey.values()) merged.push(r); + for (const r of optimisticByRkey.values()) merged.push(r); + sorted = merged.sort(compare); }; const setStatus = (next: WatchStoreStatus, nextError: Error | null = null) => { @@ -141,9 +224,14 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { const k = key(record); byKey.set(k, record); snapshotSeen?.add(k); + // If this rkey was optimistic, it's now server-confirmed. + optimisticByRkey.delete(record.rkey); }; const applyCreated = (record: WatchRecord) => { + // Drop any optimistic entry with the same rkey — the server-confirmed + // record replaces it. + optimisticByRkey.delete(record.rkey); byKey.set(key(record), record); resort(); notify(); @@ -270,10 +358,16 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { } }; + const resolveTicket = async (): Promise => { + const src = options.mintTicket; + if (!src) return null; + return typeof src === "string" ? src : await src(); + }; + const openSse = async () => { let url = options.url; - if (options.fetchTicket) { - const ticket = await options.fetchTicket(); + const ticket = await resolveTicket(); + if (ticket) { const sep = url.includes("?") ? "&" : "?"; url += `${sep}ticket=${encodeURIComponent(ticket)}`; } @@ -310,15 +404,15 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { const openWs = async () => { // Step 1: snapshot + handshake. Authenticated by the watch-scoped - // ticket from `fetchTicket`, passed as `?ticket=...`. The server + // ticket from `mintTicket`, passed as `?ticket=...`. The server // returns a ticket bound to (did, spaceUri, querySpec); we pass that // back embedded in the wsUrl on the WS upgrade so the WS itself // doesn't need any other auth. let snapshotUrl = options.url; const sep = snapshotUrl.includes("?") ? "&" : "?"; snapshotUrl += `${sep}mode=ws`; - if (options.fetchTicket) { - const ticket = await options.fetchTicket(); + const ticket = await resolveTicket(); + if (ticket) { snapshotUrl += `&ticket=${encodeURIComponent(ticket)}`; } const res = await fetch(snapshotUrl, { headers: { accept: "application/json" } }); @@ -428,14 +522,72 @@ export function createWatchStore(options: WatchStoreOptions): WatchStore { if (started) return; started = true; stopped = false; - void openOnce(); + void (async () => { + // Cache warm: populate from disk before opening the connection + // so listeners see instant records on first paint. + if (cache) { + try { + const cached = await cache.read(cacheKey); + if (cached && cached.length > 0 && !stopped) { + for (const r of cached) byKey.set(key(r), r); + resort(); + notify(); + } + } catch (err) { + log.warn?.("cache read failed", err); + } + } + if (stopped) return; + void openOnce(); + })(); }, stop() { stopped = true; closeConnections(); + if (cacheWriteTimer) { + clearTimeout(cacheWriteTimer); + cacheWriteTimer = null; + } + // Best-effort final flush so the cache reflects the last-known + // confirmed state before teardown. + void flushCache(); byKey.clear(); + optimisticByRkey.clear(); sorted = []; setStatus("closed"); + }, + addOptimistic(input) { + const now = Date.now(); + const record: WatchRecord = { + uri: input.uri ?? `at://${input.did}/${input.collection ?? ""}/${input.rkey}`, + did: input.did, + rkey: input.rkey, + collection: input.collection ?? "", + record: input.record, + time_us: input.time_us ?? now * 1000, + indexed_at: now, + cid: null, + optimistic: "pending" + }; + optimisticByRkey.set(input.rkey, record); + resort(); + notify(); + }, + markFailed(rkey, err) { + const existing = optimisticByRkey.get(rkey); + if (!existing) return; + optimisticByRkey.set(rkey, { + ...existing, + optimistic: "failed", + optimisticError: err + }); + resort(); + notify(); + }, + removeOptimistic(rkey) { + if (!optimisticByRkey.delete(rkey)) return; + resort(); + notify(); } }; } diff --git a/todo/cross-space-watch-records.md b/todo/cross-space-watch-records.md new file mode 100644 index 0000000..3f10c65 --- /dev/null +++ b/todo/cross-space-watch-records.md @@ -0,0 +1,120 @@ +# Cross-space watchRecords (actor-scoped live queries) + +## Context + +Today `.watchRecords` is per-space only — the endpoint rejects +requests without `spaceUri`: + +```ts +// src/core/router/collection.ts:563-568 +if (!spaceUri) { + return c.json( + { error: "InvalidRequest", message: "spaceUri required (cross-space watch is deferred)" }, + 400 + ); +} +``` + +This means the app's `createWatchQuery` helper only works for queries +scoped to one permissioned space. `listRecords` has no such restriction — +it already supports `?actor=` for cross-space listings. + +## The concrete gap in the group-chat example + +The channel list spans multiple spaces: each channel lives in its own +space, authored by the community DID. Current flow: + +- **SSR**: `+layout.server.ts` calls `.channel.listRecords` with + `actor=`. +- **Live updates**: `connectCommunityRealtime` in `realtime.svelte.ts` + opens an EventSource on `.realtime.subscribe?ticket=...` keyed to + `community:`, filters for `tools.atmo.chat.channel` events, and + calls `invalidateAll()` to re-run the loader. + +That works, but it means: +- Two realtime mechanisms in the app (watchRecords for messages, raw + `realtime.subscribe` + `invalidateAll` for channels). +- Channel list is re-fetched from the server on every update rather than + being incrementally maintained client-side. + +## The fix + +Allow `watchRecords` to be actor-scoped, mirroring `listRecords`: + +- Remove the "spaceUri required" gate when `actor` is supplied. +- Snapshot: reuse the existing `runPipeline` path (already handles + actor-filtered + permissioned-space union for the caller). +- Live topic: `actor:` (contrail already exposes `actorTopic`) + or `community:` (via `communityTopic` when the actor is a + registered community DID — the ticket endpoint already resolves this). +- Ticket minting: the `.realtime.ticket` endpoint already accepts + `{ topic: "community:" }` and returns a scoped ticket. + +Once done, the app reads: + +```ts +const channelsQuery = $derived( + createWatchQuery({ + endpoint: 'tools.atmo.chat.channel', + params: { actor: data.communityDid, limit: 100 } + }) +); +``` + +And `connectCommunityRealtime` + the `invalidateAll` dance goes away +(channel list is maintained incrementally; messages already use their +own per-space watch query). + +## Open question to resolve before implementing + +**ACL on the live stream.** `listRecords` filters per-space membership +on the query (via `runPipeline`'s `memberDid` path). For `watchRecords` +in this mode, events come from the pubsub via the `community:` (or +`actor:`) topic. **It needs to be verified** that the stream +actually filters events against the caller's per-space memberships — +otherwise a non-member could see `record.created` for a private channel +they're not in. + +Audit targets: +- `src/core/realtime/publishing-adapter.ts` — what topics does it publish + a space write to? If it publishes to `community:` unconditionally, + that event is delivered to every community-topic subscriber regardless + of their per-space access. +- `src/core/realtime/router.ts` / `resolveTopicForCaller` — does the + ticket issuer verify the caller's membership at ticket-mint time, or + is `community:` treated as a public-ish topic available to anyone + who can name the community? +- `src/core/router/collection.ts:653-697` — the watchRecords WS path + filters by `querySpec`; the SSE path has its own filtering. Either + way, filtering happens server-side; make sure it evaluates + per-space membership, not just collection/actor match. + +If the existing filter story is insufficient, a small gate needs to be +added: each event that leaves the stream is checked against the +caller's membership of the event's `space`, and dropped if the caller +has no access. + +## Scope estimate + +- If ACL is already correct: ~1 hour. Small change to remove the + spaceUri gate and wire actor → topic resolution. +- If ACL filter needs adding: ~half-day. Introduce a per-event + membership check in the stream's filter path and cache memberships + on the connection to keep it cheap. + +## Out of scope for this work + +- Combining multiple `actor` values (single-actor queries are enough + for the community-channels use case). +- Hydration relations across the actor-scoped snapshot — same machinery + as today; just confirm it works without `spaceUri` context. + +## Cleanup after this ships + +- Delete `connectCommunityRealtime` from the example's + `lib/rooms/realtime.svelte.ts` (and its ticket fetching helper). +- `+layout.server.ts` stops fetching channels and pass them via page + data; the layout uses `createWatchQuery` directly. +- Consider whether `channelMessages` (the per-space message store) is + still needed — it's there for the prior SSR-message pattern; messages + are now read directly from `messagesQuery.records`. Probably delete. diff --git a/todo/realtime-do-dev-wiring.md b/todo/realtime-do-dev-wiring.md new file mode 100644 index 0000000..af22e81 --- /dev/null +++ b/todo/realtime-do-dev-wiring.md @@ -0,0 +1,104 @@ +# Fix DO wiring in Vite dev (currently papered over with a `dev` flag) + +## Context + +In `examples/sveltekit-group-chat/src/lib/contrail/index.ts` the realtime +PubSub is selected with: + +```ts +const pubsub = dev ? new InMemoryPubSub() : new DurableObjectPubSub(env.REALTIME); +``` + +That `dev` check is standing in for "the `RealtimePubSubDO` class isn't +actually registered with miniflare right now." It happens to line up with +Vite dev today, but it's not really a dev-vs-prod concern — it's a +build-wiring concern. + +### Why the DO isn't available in Vite dev + +- `wrangler.jsonc` declares the DO binding `REALTIME` with + `class_name: "RealtimePubSubDO"` and `main: ".svelte-kit/cloudflare/_worker.js"`. +- `@sveltejs/adapter-cloudflare@7.x` sets up miniflare via `platformProxy` + automatically in dev, so `platform.env.REALTIME` is bound. +- But the class lives inside the built worker. It's re-exported into + `_worker.js` by `scripts/append-scheduled.ts` *after* `pnpm build`. +- Vite dev doesn't run that post-build step, so the DO binding points at + nothing and `stub.fetch(...)` crashes with `Internal Error`. + +## Three fixes, smallest → largest + +### 1. App-level probe (smallest) + +In the example only. Add a helper that tries a cheap `stub.fetch(health)` +at bundle-build time and falls back to `InMemoryPubSub` if it throws. + +```ts +async function pickPubSub(ns: DurableObjectNamespace | undefined): Promise { + if (!ns) return new InMemoryPubSub(); + try { + const stub = ns.get(ns.idFromName('__probe__')); + const res = await stub.fetch('http://probe/__health', { signal: AbortSignal.timeout(250) }); + if (res.ok) return new DurableObjectPubSub(ns); + } catch {} + return new InMemoryPubSub(); +} +``` + +- Pro: no library change, no build change. +- Con: async bundle construction (currently sync), every app using contrail + has to reinvent this. + +### 2. Library helper with probe (medium, recommended) + +Ship the probe in `@atmo-dev/contrail` as something like: + +```ts +export async function resolveRealtimePubSub( + ns?: DurableObjectNamespace, + fallback: () => PubSub = () => new InMemoryPubSub(), +): Promise +``` + +Every app using contrail gets the correct behavior for free. Still async +construction, but now that lives at the library boundary where it's +easier to swallow. + +- Pro: reusable primitive, fixes the smell at the right layer. +- Con: contrail now has opinions about "is the DO actually usable"; async + bundle setup ripples to every consumer (our current `getBundle` is sync). + +### 3. Proper dev-prod parity (largest, most correct) + +Actually register the DO class with miniflare in Vite dev so `env.REALTIME` +works identically to prod. + +- Restructure the DO re-export so it lives at a stable source path (no + post-build patching). +- Configure `adapter({ platformProxy: { ... } })` with miniflare options + that point at that stable entry for DO hosting. Likely needs a + dedicated DO worker script in miniflare's `workers` config. +- Adjust `scripts/append-scheduled.ts` so it no longer duplicates the + re-export; it only keeps the `scheduled` handler append. +- Drop the `dev ? InMemory : DO` branch in the example entirely — always + `new DurableObjectPubSub(env.REALTIME)`. + +- Pro: dev === prod, no branching anywhere, also unblocks WS-in-dev + (which today forces `transport: dev ? 'sse' : 'ws'` in + `+page.svelte`). +- Con: touches build config, wrangler config, and adapter-cloudflare + internals. Non-trivial to get right; worth its own session. + +## Related cleanup (do in the same pass) + +- Once the DO works in dev, drop `transport: dev ? 'sse' : 'ws'` at + `src/routes/c/[communityDid]/[channelKey]/+page.svelte` — always `'ws'`. +- Remove the `dev` import from `src/lib/contrail/index.ts` and + `+page.svelte` (only remaining consumer). +- Revisit whether `InMemoryPubSub` should stay exported at all from the + example config; if option 3 lands, it's only needed for non-CF hosts. + +## Recommendation + +Go with **(2)** next. It's the right layer, and the async-construction +cost is modest (one-time probe at first use). Tackle (3) when we do the +broader "unify transport" pass. diff --git a/tsup.config.ts b/tsup.config.ts index 743ec36..1bbaa88 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ "src/generate.ts", "src/publish.ts", "src/sync/index.ts", + "src/sync/cache-idb.ts", "src/adapters/sqlite.ts", "src/adapters/postgres.ts", ], -- 2.51.2 From 7a5bde07f6c8923703a6c4d2b65356c3048ebc41 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:18:06 +0200 Subject: [PATCH 14/18] commit --- .../src/lib/rooms/channels-context.ts | 21 ++ .../src/lib/rooms/realtime.svelte.ts | 7 +- .../src/lib/rooms/rooms.remote.ts | 3 +- .../src/lib/rooms/server.ts | 15 +- .../src/lib/rooms/watch.svelte.ts | 5 +- .../routes/c/[communityDid]/+layout.server.ts | 64 +---- .../routes/c/[communityDid]/+layout.svelte | 69 ++++- .../src/routes/c/[communityDid]/+page.svelte | 12 +- .../[communityDid]/[channelKey]/+page.svelte | 11 +- src/core/realtime/ticket.ts | 6 +- src/core/router/collection.ts | 255 +++++++++++++----- src/core/router/index.ts | 8 +- 12 files changed, 330 insertions(+), 146 deletions(-) create mode 100644 examples/sveltekit-group-chat/src/lib/rooms/channels-context.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/channels-context.ts b/examples/sveltekit-group-chat/src/lib/rooms/channels-context.ts new file mode 100644 index 0000000..fe350f8 --- /dev/null +++ b/examples/sveltekit-group-chat/src/lib/rooms/channels-context.ts @@ -0,0 +1,21 @@ +/** Context key + type for the community channel list. + * + * The community layout (`+layout.svelte`) maintains a live channel list via + * a cross-space `createWatchQuery` on `tools.atmo.chat.channel`. Child pages + * read the list through Svelte context so they don't each run their own + * watch query. The getter ensures reads stay reactive. */ + +export interface ChannelMeta { + spaceUri: string; + key: string; + name: string; + topic?: string; + visibility: 'public' | 'private'; + createdAt: string; +} + +export interface ChannelsContext { + readonly list: readonly ChannelMeta[]; +} + +export const CHANNELS_CTX = Symbol('community-channels'); diff --git a/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts index 70bd04a..db74c8b 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts @@ -138,17 +138,16 @@ export function connectCommunityRealtime(communityDid: string): () => void { bumpUnread(p.space, rec.createdAt); } } - } else if (p.collection === 'tools.atmo.chat.channel') { - void invalidateAll(); } + // Channel record events are handled by the layout's channel + // watch query now — no server-loader reinvoke needed. } else if (kind === 'record.deleted') { const p = ev.payload as { uri: string; did: string; collection: string; rkey: string; space?: string }; if (!p.space) return; if (p.collection === 'tools.atmo.chat.message') { channelMessages.remove(p.space, p.rkey); - } else if (p.collection === 'tools.atmo.chat.channel') { - void invalidateAll(); } + // Channel deletions: handled by the layout's channel watch query. } else if (kind === 'member.added' || kind === 'member.removed') { void invalidateAll(); } diff --git a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts index 2f8402e..44ae8ce 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts @@ -189,7 +189,8 @@ export const postMessage = command(PostMessageInput, async (input) => { // --------------------------------------------------------------------------- const MintWatchTicketInput = v.object({ - spaceUri: v.pipe(v.string(), v.minLength(1)), + spaceUri: v.optional(v.pipe(v.string(), v.minLength(1))), + actor: v.optional(v.pipe(v.string(), v.minLength(1))), watchRecordsNsid: v.pipe(v.string(), v.minLength(1)), limit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(200))) }); diff --git a/examples/sveltekit-group-chat/src/lib/rooms/server.ts b/examples/sveltekit-group-chat/src/lib/rooms/server.ts index c81e4ac..0d42c98 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/server.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/server.ts @@ -198,10 +198,21 @@ export async function getRealtimeTicket( * snapshot on the ticketed connection. */ export async function mintWatchTicket( ctx: AuthedCallContext, - input: { watchRecordsNsid: string; spaceUri: string; limit?: number } + input: { + watchRecordsNsid: string; + /** Per-space watch. Mutually exclusive with `actor`. */ + spaceUri?: string; + /** Cross-space watch — actor must currently be a community DID. */ + actor?: string; + limit?: number; + } ): Promise<{ ticket: string; expiresAt: number }> { + if (!input.spaceUri && !input.actor) { + throw new Error('mintWatchTicket: spaceUri or actor required'); + } const url = new URL(`http://localhost/xrpc/${input.watchRecordsNsid}`); - url.searchParams.set('spaceUri', input.spaceUri); + if (input.spaceUri) url.searchParams.set('spaceUri', input.spaceUri); + if (input.actor) url.searchParams.set('actor', input.actor); url.searchParams.set('mode', 'ws'); if (input.limit) url.searchParams.set('limit', String(input.limit)); const req = new Request(url, { headers: { accept: 'application/json' } }); diff --git a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts b/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts index 34057ea..85c2ddb 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts +++ b/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts @@ -91,9 +91,12 @@ export interface TicketMintContext { export type TicketMinter = (ctx: TicketMintContext) => Promise; let ticketMinter: TicketMinter | null = async ({ endpoint, params }) => { + const spaceUri = params.spaceUri ? String(params.spaceUri) : undefined; + const actor = params.actor ? String(params.actor) : undefined; const res = await mintWatchTicketCmd({ watchRecordsNsid: `${endpoint}.watchRecords`, - spaceUri: String(params.spaceUri ?? ''), + ...(spaceUri ? { spaceUri } : {}), + ...(actor ? { actor } : {}), limit: typeof params.limit === 'number' ? params.limit : 50 }); return res.ticket; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts index ff8f27b..e97550f 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts @@ -1,8 +1,7 @@ import type { LayoutServerLoad } from './$types'; import { redirect } from '@sveltejs/kit'; -import type { Client } from '@atcute/client'; import { authedFetch } from '$lib/rooms/server'; -import { parseSpaceUri, buildAdminUri } from '$lib/rooms/uri'; +import { buildAdminUri } from '$lib/rooms/uri'; interface ServerMeta { communityDid: string; @@ -12,25 +11,12 @@ interface ServerMeta { membersUri: string; } -interface ChannelMeta { - spaceUri: string; - key: string; - name: string; - topic?: string; - visibility: 'public' | 'private'; - createdAt: string; -} - export const load: LayoutServerLoad = async ({ locals, params, platform }) => { - if (!locals.did || !locals.client) { + if (!locals.did) { throw redirect(302, '/'); } const communityDid = decodeURIComponent(params.communityDid); - const ctx = { - env: platform!.env, - client: locals.client as Client, - did: locals.did as string - }; + const ctx = { env: platform!.env, did: locals.did as string }; // --- fetch server record ------------------------------------------------- let server: ServerMeta | null = null; @@ -73,44 +59,10 @@ export const load: LayoutServerLoad = async ({ locals, params, platform }) => { // fallthrough — server stays null, UI shows "server" fallback } - // --- fetch channels ------------------------------------------------------ - const channels: ChannelMeta[] = []; - try { - const data = await authedFetch<{ - records: Array<{ - did: string; - rkey: string; - record: { - communityDid?: string; - name?: string; - topic?: string; - visibility?: 'public' | 'private'; - createdAt?: string; - }; - space?: string; - }>; - }>(ctx, 'tools.atmo.chat.channel.listRecords', { - query: { actor: communityDid, limit: '100' } - }); - for (const r of data.records) { - if (!r.space || r.did !== communityDid || r.rkey !== 'self') continue; - if (r.record?.communityDid !== communityDid) continue; - if (!r.record.name || !r.record.visibility || !r.record.createdAt) continue; - const parsed = parseSpaceUri(r.space); - if (!parsed || parsed.key.startsWith('$') || parsed.key === 'members') continue; - channels.push({ - spaceUri: r.space, - key: parsed.key, - name: r.record.name, - topic: r.record.topic, - visibility: r.record.visibility, - createdAt: r.record.createdAt - }); - } - channels.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); - } catch { - // fallthrough - } + // Channels are no longer fetched here — the layout now derives them from + // a live `createWatchQuery` against `tools.atmo.chat.channel` scoped by + // actor=. New/renamed/deleted channels reflect instantly + // without an `invalidateAll()` → server-loader roundtrip. // --- caller's access level on $admin ----------------------------------- let isAdmin = false; @@ -126,5 +78,5 @@ export const load: LayoutServerLoad = async ({ locals, params, platform }) => { // stay false } - return { communityDid, server, channels, isAdmin, myDid: locals.did }; + return { communityDid, server, isAdmin, myDid: locals.did }; }; diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte index 27980bc..7bdbf64 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte @@ -1,13 +1,17 @@
- {#if data.channels.length === 0} + {#if channelsCtx.list.length === 0}

No channels yet.

{#if data.isAdmin} diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte index 447da11..13da1b9 100644 --- a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte +++ b/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte @@ -6,19 +6,20 @@ import { setCurrentChannel } from '$lib/rooms/realtime.svelte'; import { markLastRead } from '$lib/rooms/unread.svelte'; import { displayName } from '$lib/rooms/profiles.svelte'; + import { getContext } from 'svelte'; import { createWatchQuery } from '$lib/rooms/watch.svelte'; import { setConnectionStatus, resetConnectionStatus } from '$lib/rooms/connection.svelte'; import { nextTid } from '@atmo-dev/contrail'; + import { CHANNELS_CTX, type ChannelsContext } from '$lib/rooms/channels-context'; let { data } = $props(); - let channel = $derived(data.channels.find((c) => c.key === data.channelKey)); + const channelsCtx = getContext(CHANNELS_CTX); + + let channel = $derived(channelsCtx.list.find((c) => c.key === data.channelKey)); let channelName = $derived(channel?.name ?? data.channelKey); - // Live message feed. `$derived` recreates the query when spaceUri changes; - // the old instance is auto-torn down via `createSubscriber` once no - // component reads it. Initial ticket is pre-minted during SSR to save one - // roundtrip; reconnects call the configured ticket minter transparently. + // Live message feed. `$derived` recreates the query when spaceUri changes let messagesQuery = $derived( createWatchQuery({ endpoint: 'tools.atmo.chat.message', diff --git a/src/core/realtime/ticket.ts b/src/core/realtime/ticket.ts index 7e0fd6a..1c6e3de 100644 --- a/src/core/realtime/ticket.ts +++ b/src/core/realtime/ticket.ts @@ -26,7 +26,11 @@ export interface TicketPayload { export interface TicketQuerySpec { collection: string; - spaceUri: string; + /** Exactly one of `spaceUri` or `actor` is set. `spaceUri` = per-space + * watch; `actor` = cross-space watch for records authored by this DID + * (the ticket's `topics` list carries the expanded delivery topics). */ + spaceUri?: string; + actor?: string; hydrate?: Record; } diff --git a/src/core/router/collection.ts b/src/core/router/collection.ts index 1918aa8..d54f6d8 100644 --- a/src/core/router/collection.ts +++ b/src/core/router/collection.ts @@ -23,19 +23,41 @@ import type { SpacesContext } from "."; import type { Nsid } from "@atcute/lexicons"; import type { RealtimeEvent } from "../realtime/types"; import { sseResponse } from "../realtime/sse"; -import { spaceTopic } from "../realtime/types"; +import { spaceTopic, communityTopic, parseSpaceTopic } from "../realtime/types"; import type { SubscriberQuerySpec } from "../realtime/durable-object"; import { DurableObjectPubSub } from "../realtime/durable-object"; import { TicketSigner, type TicketQuerySpec } from "../realtime/ticket"; +import { resolveTopicForCaller } from "../realtime/resolve"; +import { mergeAsyncIterables } from "../realtime/merge"; +import type { CommunityAdapter } from "../community/adapter"; import { getRelationField, getNestedValue } from "../types"; +/** Scope of a watch stream. + * - `space`: single permissioned space — one `space:` topic. + * - `actor`: records authored by `actor` across multiple spaces — the + * resolver expanded these to a per-caller subset of space topics (plus + * `actor:` for public records). Events outside `allowedSpaces` + * are filtered out. */ +type WatchScope = + | { kind: "space"; spaceUri: string } + | { + kind: "actor"; + actor: string; + /** Concrete pubsub topics to subscribe to (from resolveTopicForCaller). */ + topics: string[]; + /** Space URIs the caller can see. Events with `space` outside this + * set are dropped. Undefined `space` on an event (public record) + * is allowed only when `actor` topic is in `topics`. */ + allowedSpaces: Set; + }; + /** Shared implementation of the watchRecords snapshot+live loop. Called by * both transport branches (SSE and Worker-terminated WS). The caller owns * the actual socket/stream and provides a `send(kind, data)` closure. */ async function runQueryStream(opts: { send: (kind: string, data: unknown) => void; abort: AbortController; - spaceUri: string; + scope: WatchScope; callerDid: string | undefined; params: URLSearchParams; db: Database; @@ -53,7 +75,7 @@ async function runQueryStream(opts: { const { send, abort, - spaceUri, + scope, callerDid, params, db, @@ -66,6 +88,13 @@ async function runQueryStream(opts: { childCollectionMap } = opts; + // Predicate: does this event belong in the caller's scope? + const inScope = (space: string | undefined): boolean => { + if (scope.kind === "space") return space === scope.spaceUri; + if (space == null) return false; // actor mode: require space for now (app topic) + return scope.allowedSpaces.has(space); + }; + const hydrateSpec = parseHydrateParams(params, relations, references); const trackHydration = Object.keys(hydrateSpec.relations).length > 0; const parentUris = new Set(); @@ -80,7 +109,11 @@ async function runQueryStream(opts: { const meta = childCollectionMap.get(event.payload.collection); if (!meta) return; if (!(hydrateSpec.relations as Record)[meta.relName]) return; - if (event.payload.space !== spaceUri) return; + if (!inScope(event.payload.space)) return; + // Actor mode: additionally require the record's author match our actor + // (the caller might share spaces with other authors — we only surface + // records by the actor under watch). + if (scope.kind === "actor" && event.payload.did !== scope.actor) return; if (event.kind === "record.created") { const matched = getNestedValue(event.payload.record, meta.matchField); @@ -108,7 +141,7 @@ async function runQueryStream(opts: { collection: event.payload.collection, cid: event.payload.cid, record: event.payload.record, - _space: spaceUri + _space: event.payload.space } }); } else { @@ -132,7 +165,8 @@ async function runQueryStream(opts: { return; } if (event.kind !== "record.created" && event.kind !== "record.deleted") return; - if (event.payload.space !== spaceUri) return; + if (!inScope(event.payload.space)) return; + if (scope.kind === "actor" && event.payload.did !== scope.actor) return; if (event.payload.collection !== colNsid) { handleChildEvent(event); @@ -154,7 +188,7 @@ async function runQueryStream(opts: { record: event.payload.record, time_us: nowUs, indexed_at: event.ts, - _space: spaceUri + _space: event.payload.space } }); } else { @@ -167,8 +201,15 @@ async function runQueryStream(opts: { } }; - const topic = spaceTopic(spaceUri); - const iter = pubsub.subscribe(topic, abort.signal); + // Subscribe: one topic for space-scoped, merge across all topics for + // actor-scoped. `mergeAsyncIterables` exists for exactly this case. + let iter: AsyncIterable; + if (scope.kind === "space") { + iter = pubsub.subscribe(spaceTopic(scope.spaceUri), abort.signal); + } else { + const sources = scope.topics.map((t) => pubsub.subscribe(t, abort.signal)); + iter = mergeAsyncIterables(sources, abort.signal); + } const buffered: RealtimeEvent[] = []; let snapshotDone = false; @@ -186,8 +227,15 @@ async function runQueryStream(opts: { })(); try { - send("snapshot.start", { spaceUri, collection: colNsid }); - const result = await runPipeline(db, config, collection, params, undefined, [spaceUri]); + send( + "snapshot.start", + scope.kind === "space" + ? { spaceUri: scope.spaceUri, collection: colNsid } + : { actor: scope.actor, collection: colNsid } + ); + const snapshotSpaces = + scope.kind === "space" ? [scope.spaceUri] : Array.from(scope.allowedSpaces); + const result = await runPipeline(db, config, collection, params, undefined, snapshotSpaces); for (const record of result.records) { if (abort.signal.aborted) break; if (typeof record.uri === "string") parentUris.add(record.uri); @@ -209,7 +257,15 @@ async function runQueryStream(opts: { } } } - send("snapshot.record", { record }); + // Normalize `space` → `_space` so snapshot records carry the same + // field name as live `record.created` payloads. Clients then have a + // single key to read regardless of origin. + const normalized = record as Record; + if (normalized.space !== undefined && normalized._space === undefined) { + normalized._space = normalized.space; + delete normalized.space; + } + send("snapshot.record", { record: normalized }); } send("snapshot.end", { cursor: result.cursor }); snapshotDone = true; @@ -397,10 +453,14 @@ export function registerCollectionRoutes( db: Database, config: ContrailConfig, spacesCtx?: SpacesContext | null, - options: { pubsub?: import("../realtime/types").PubSub | null } = {} + options: { + pubsub?: import("../realtime/types").PubSub | null; + community?: CommunityAdapter | null; + } = {} ): void { const ns = config.namespace; const pubsub = options.pubsub ?? null; + const community = options.community ?? null; /** When a per-collection endpoint receives `?spaceUri=...`, verify the JWT, * resolve membership, run the space ACL, and return the caller DID if allowed. @@ -561,67 +621,128 @@ export function registerCollectionRoutes( app.get(`/xrpc/${ns}.${collection}.watchRecords`, async (c) => { const params = new URL(c.req.url).searchParams; const spaceUri = params.get("spaceUri"); - if (!spaceUri) { + const actorParam = params.get("actor"); + + if (!spaceUri && !actorParam) { return c.json( - { error: "InvalidRequest", message: "spaceUri required (cross-space watch is deferred)" }, + { error: "InvalidRequest", message: "spaceUri or actor required" }, 400 ); } - // Try ticket-auth first: if a valid watch ticket scoped to this - // spaceUri/collection is present, use it and skip the JWT gate. + // Resolve the caller and their scope. Two parallel paths: + // - space-scoped: single `space:` topic, per-space ACL gate. + // - actor-scoped: caller's reachable spaces in the actor's + // community (v1 only supports community DIDs as the actor). + // Events are delivered via N `space:` topics and filtered + // to `did === actor`. let callerDid: string | undefined; - let querySpec: SubscriberQuerySpec; - let ticketSpec: SubscriberQuerySpec | null = null; + let scope: WatchScope; + let scopeTopics: string[]; // for ticket signing + let ticketSpec: TicketQuerySpec | null = null; const providedTicket = params.get("ticket"); if (providedTicket && ticketSigner) { const payload = await ticketSigner.verify(providedTicket); - if (payload?.querySpec) { + if (payload?.querySpec && payload.querySpec.collection === colNsid) { const ts = payload.querySpec; - if ( - ts.collection === colNsid && - ts.spaceUri === spaceUri && - payload.topics.includes(spaceTopic(spaceUri)) - ) { + if (spaceUri && ts.spaceUri === spaceUri) { + if (payload.topics.includes(spaceTopic(spaceUri))) { + callerDid = payload.did; + ticketSpec = { + collection: ts.collection, + spaceUri: ts.spaceUri, + ...(ts.hydrate ? { hydrate: ts.hydrate } : {}) + }; + } + } else if (actorParam && ts.actor === actorParam) { callerDid = payload.did; ticketSpec = { collection: ts.collection, - spaceUri: ts.spaceUri, + actor: ts.actor, ...(ts.hydrate ? { hydrate: ts.hydrate } : {}) }; } } } - if (!ticketSpec) { - const gated = await gateSpaceAccess(c, spaceUri, "read"); - if (gated instanceof Response) return gated; - callerDid = "callerDid" in gated ? gated.callerDid : undefined; + const hydrateSpec = parseHydrateParams(params, relations, references); + const hydrateForSpec = Object.keys(hydrateSpec.relations).length > 0 + ? Object.fromEntries( + Object.entries(hydrateSpec.relations).map(([relName]) => { + const rel = relations[relName]!; + const childNsid = + nsidForShortName(config, rel.collection) ?? rel.collection; + return [ + relName, + { childCollection: childNsid, matchField: getRelationField(rel) } + ]; + }) + ) + : undefined; + + if (spaceUri) { + if (!ticketSpec) { + const gated = await gateSpaceAccess(c, spaceUri, "read"); + if (gated instanceof Response) return gated; + callerDid = "callerDid" in gated ? gated.callerDid : undefined; + } + scope = { kind: "space", spaceUri }; + scopeTopics = [spaceTopic(spaceUri)]; + } else { + // Actor-scoped path — v1 only supports community DIDs. + const actor = actorParam!; + if (!community || !spacesCtx) { + return c.json( + { error: "NotSupported", reason: "community-module-disabled" }, + 400 + ); + } + const isCommunity = !!(await community.getCommunity(actor)); + if (!isCommunity) { + return c.json( + { error: "InvalidRequest", reason: "actor-must-be-community-did", message: "cross-space watch currently only supports community DIDs as actor" }, + 400 + ); + } + + if (!ticketSpec) { + // Verify the caller via the same JWT/in-process path used for + // per-space queries, then resolve the community topic to the + // caller's accessible space topics. + const nsidLxm = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsidLxm); + if (!auth) { + return c.json( + { error: "AuthRequired", message: "service-auth JWT or in-process principal required" }, + 401 + ); + } + callerDid = auth.issuer; + } + const resolved = await resolveTopicForCaller(communityTopic(actor), callerDid!, { + spaces: spacesCtx.adapter, + community + }); + if (!resolved.ok) { + const status = + resolved.error === "NotFound" ? 404 : + resolved.error === "Forbidden" ? 403 : 400; + return c.json({ error: resolved.error, reason: resolved.reason }, status); + } + const allowedSpaces = new Set(); + for (const t of resolved.topics) { + const uri = parseSpaceTopic(t); + if (uri) allowedSpaces.add(uri); + } + scope = { kind: "actor", actor, topics: resolved.topics, allowedSpaces }; + scopeTopics = resolved.topics; } - // Build the query spec the DO will filter events against. Prefer - // the ticket's spec when present (guarantees parity with what the - // client asked for at handshake time, no param drift). - const hydrateSpec = parseHydrateParams(params, relations, references); - querySpec = ticketSpec ?? { + const querySpec: TicketQuerySpec = ticketSpec ?? { collection: colNsid, - spaceUri, - ...(Object.keys(hydrateSpec.relations).length > 0 - ? { - hydrate: Object.fromEntries( - Object.entries(hydrateSpec.relations).map(([relName]) => { - const rel = relations[relName]!; - const childNsid = - nsidForShortName(config, rel.collection) ?? rel.collection; - return [ - relName, - { childCollection: childNsid, matchField: getRelationField(rel) } - ]; - }) - ) - } - : {}) + ...(spaceUri ? { spaceUri } : { actor: actorParam! }), + ...(hydrateForSpec ? { hydrate: hydrateForSpec } : {}) }; // Upgrade-to-WS path — forward directly to the DO with the spec, @@ -634,29 +755,27 @@ export function registerCollectionRoutes( if (isWsMode && !isUpgrade) { // Handshake: return snapshot + a ticket the client uses to - // upgrade. Ticket carries the (did, topic, querySpec) signed + // upgrade. Ticket carries the (did, topics, querySpec) signed // so the WS-upgrade route skips any other auth. try { - // Capture a server-side timestamp BEFORE running the snapshot. - // Any event published after this moment will have ts > sinceTs - // and be replayed by the DO on WS connect — so the client - // never misses events during the snapshot→WS gap. const sinceTs = Date.now(); + const snapshotSpaces = + scope.kind === "space" ? [scope.spaceUri] : Array.from(scope.allowedSpaces); const result = await runPipeline( db, config, collection, params, undefined, - [spaceUri] + snapshotSpaces ); let ticket: string | undefined; if (ticketSigner && callerDid) { ticket = await ticketSigner.sign({ - topics: [spaceTopic(spaceUri)], + topics: scopeTopics, did: callerDid, ttlMs: ticketTtl, - querySpec: querySpec as TicketQuerySpec + querySpec }); } const wsUrl = (() => { @@ -683,15 +802,23 @@ export function registerCollectionRoutes( } } - if (isUpgrade && pubsub instanceof DurableObjectPubSub) { + if (isUpgrade && pubsub instanceof DurableObjectPubSub && scope.kind === "space") { // Forward the WS upgrade to the DO. The DO owns the socket from // here and hibernates when idle. Replays any events buffered // since the handshake `sinceTs` so the client closes the gap. + // + // Actor-scoped queries fall through to the worker-terminated + // path below — the DO binding is single-topic today; extending + // it to fan out over N topics is future work. const sinceTsParam = params.get("sinceTs"); const sinceTs = sinceTsParam ? Number(sinceTsParam) : 0; - return pubsub.forwardSubscribe(spaceTopic(spaceUri), c.req.raw, { + return pubsub.forwardSubscribe(spaceTopic(scope.spaceUri), c.req.raw, { did: callerDid, - querySpec, + querySpec: { + collection: querySpec.collection, + spaceUri: scope.spaceUri, + ...(querySpec.hydrate ? { hydrate: querySpec.hydrate } : {}) + }, sinceTs: Number.isFinite(sinceTs) ? sinceTs : 0 }); } @@ -733,7 +860,7 @@ export function registerCollectionRoutes( void runQueryStream({ send: sendWs, abort: ac, - spaceUri, + scope, callerDid, params, db, @@ -802,7 +929,7 @@ export function registerCollectionRoutes( void runQueryStream({ send, abort: ac, - spaceUri, + scope, callerDid, params, db, diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 8a1b732..bf714fd 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -114,7 +114,13 @@ export function createApp( } registerAdminRoutes(app, db, config); - registerCollectionRoutes(app, db, config, spacesCtx, { pubsub: realtimePubsub }); + const communityAdapterForCollection = config.community + ? new CommunityAdapter(spacesDb) + : null; + registerCollectionRoutes(app, db, config, spacesCtx, { + pubsub: realtimePubsub, + community: communityAdapterForCollection, + }); registerFeedRoutes(app, db, config); registerNotifyRoute(app, db, config); const communityAdapterForSpaces = config.community && spacesCtx -- 2.51.2 From 8944bb8740f7064b247c53825205304d78215869 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:29:43 +0200 Subject: [PATCH 15/18] monorepo --- .changeset/config.json | 2 +- .github/workflows/ci.yml | 48 + .github/workflows/release.yml | 14 +- .gitignore | 11 +- README.md | 453 +- .../cloudflare-workers/.gitignore | 0 .../cloudflare-workers/README.md | 0 .../cloudflare-workers/config.ts | 0 .../cloudflare-workers/generate.ts | 2 +- .../cloudflare-workers/package.json | 3 +- {examples => apps}/cloudflare-workers/sync.ts | 0 .../cloudflare-workers/tsconfig.json | 0 .../cloudflare-workers/worker.ts | 0 .../cloudflare-workers/wrangler.jsonc | 0 .../group-chat}/.claude/settings.local.json | 0 .../group-chat}/.env.example | 0 .../group-chat}/.gitignore | 0 .../group-chat}/.npmrc | 0 .../group-chat}/.prettierignore | 0 .../group-chat}/.prettierrc | 0 .../group-chat}/LICENSE | 0 .../group-chat}/README.md | 0 .../group-chat}/eslint.config.js | 0 .../tools/atmo/chat/channel/getRecord.json | 0 .../tools/atmo/chat/channel/listRecords.json | 0 .../tools/atmo/chat/channel/watchRecords.json | 0 .../tools/atmo/chat/community/adopt.json | 0 .../tools/atmo/chat/community/defs.json | 0 .../tools/atmo/chat/community/delete.json | 0 .../atmo/chat/community/deleteRecord.json | 0 .../tools/atmo/chat/community/getHealth.json | 0 .../tools/atmo/chat/community/list.json | 0 .../tools/atmo/chat/community/mint.json | 0 .../tools/atmo/chat/community/putRecord.json | 0 .../tools/atmo/chat/community/reauth.json | 0 .../atmo/chat/community/space/create.json | 0 .../atmo/chat/community/space/delete.json | 0 .../chat/community/space/deleteRecord.json | 0 .../atmo/chat/community/space/grant.json | 0 .../chat/community/space/listMembers.json | 0 .../atmo/chat/community/space/putRecord.json | 0 .../atmo/chat/community/space/resync.json | 0 .../atmo/chat/community/space/revoke.json | 0 .../chat/community/space/setAccessLevel.json | 0 .../tools/atmo/chat/getCursor.json | 0 .../tools/atmo/chat/getOverview.json | 0 .../tools/atmo/chat/getProfile.json | 0 .../tools/atmo/chat/invite/create.json | 0 .../tools/atmo/chat/invite/defs.json | 0 .../tools/atmo/chat/invite/list.json | 0 .../tools/atmo/chat/invite/redeem.json | 0 .../tools/atmo/chat/invite/revoke.json | 0 .../tools/atmo/chat/message/getRecord.json | 0 .../tools/atmo/chat/message/listRecords.json | 0 .../tools/atmo/chat/message/watchRecords.json | 0 .../tools/atmo/chat/notifyOfUpdate.json | 0 .../tools/atmo/chat/permissionSet.json | 0 .../tools/atmo/chat/realtime/subscribe.json | 0 .../tools/atmo/chat/realtime/ticket.json | 0 .../tools/atmo/chat/server/getRecord.json | 0 .../tools/atmo/chat/server/listRecords.json | 0 .../tools/atmo/chat/server/watchRecords.json | 0 .../tools/atmo/chat/space/addMember.json | 0 .../tools/atmo/chat/space/createSpace.json | 0 .../tools/atmo/chat/space/defs.json | 0 .../tools/atmo/chat/space/deleteRecord.json | 0 .../tools/atmo/chat/space/getBlob.json | 0 .../tools/atmo/chat/space/getRecord.json | 0 .../tools/atmo/chat/space/getSpace.json | 0 .../tools/atmo/chat/space/leaveSpace.json | 0 .../tools/atmo/chat/space/listBlobs.json | 0 .../tools/atmo/chat/space/listMembers.json | 0 .../tools/atmo/chat/space/listRecords.json | 0 .../tools/atmo/chat/space/listSpaces.json | 0 .../tools/atmo/chat/space/putRecord.json | 0 .../tools/atmo/chat/space/removeMember.json | 0 .../tools/atmo/chat/space/uploadBlob.json | 0 .../tools/atmo/chat/spaceExt/whoami.json | 0 .../group-chat}/lexicons-pulled/README.md | 0 .../app/bsky/actor/profile.json | 0 .../lexicons/tools/atmo/chat/channel.json | 0 .../lexicons/tools/atmo/chat/message.json | 0 .../lexicons/tools/atmo/chat/server.json | 0 .../group-chat}/package.json | 2 + .../group-chat}/pnpm-lock.yaml | 0 .../group-chat}/scripts/append-scheduled.ts | 0 .../group-chat}/scripts/generate.ts | 2 +- .../group-chat}/scripts/sync.ts | 0 .../group-chat}/src/app.css | 0 .../group-chat}/src/app.d.ts | 0 .../group-chat}/src/app.html | 0 .../group-chat}/src/hooks.server.ts | 0 .../src/lib/atproto/auth.svelte.ts | 0 .../src/lib/atproto/generated-methods.ts | 0 .../src/lib/atproto/image-helper.ts | 0 .../group-chat}/src/lib/atproto/index.ts | 0 .../group-chat}/src/lib/atproto/methods.ts | 0 .../group-chat}/src/lib/atproto/port.ts | 0 .../src/lib/atproto/scripts/generate-key.ts | 0 .../lib/atproto/scripts/generate-secret.ts | 0 .../src/lib/atproto/scripts/setup-dev.ts | 0 .../src/lib/atproto/scripts/tunnel.ts | 0 .../src/lib/atproto/server/kv-store.ts | 0 .../src/lib/atproto/server/oauth.remote.ts | 0 .../src/lib/atproto/server/oauth.ts | 0 .../src/lib/atproto/server/profile.ts | 0 .../src/lib/atproto/server/repo.remote.ts | 0 .../src/lib/atproto/server/session.ts | 0 .../src/lib/atproto/server/signed-cookie.ts | 0 .../group-chat}/src/lib/atproto/settings.ts | 0 .../src/lib/atproto/ui/LoginModal.svelte | 0 .../group-chat}/src/lib/contrail/client.ts | 0 .../group-chat}/src/lib/contrail/config.ts | 0 .../group-chat}/src/lib/contrail/index.ts | 0 .../group-chat}/src/lib/emojis/icons.json | 0 .../group-chat}/src/lib/emojis/index.ts | 0 .../group-chat}/src/lib/index.ts | 0 .../src/lib/rooms/channels-context.ts | 0 .../src/lib/rooms/connection.svelte.ts | 2 +- .../src/lib/rooms/invites.remote.ts | 0 .../src/lib/rooms/profiles.svelte.ts | 0 .../src/lib/rooms/realtime.svelte.ts | 0 .../group-chat}/src/lib/rooms/rooms.remote.ts | 0 .../group-chat}/src/lib/rooms/server.ts | 0 .../src/lib/rooms/unread.svelte.ts | 0 .../group-chat}/src/lib/rooms/uri.ts | 0 .../group-chat}/src/lib/rooms/watch.svelte.ts | 6 +- .../oauth-client-metadata.json/+server.ts | 0 .../routes/(oauth)/oauth/callback/+server.ts | 0 .../routes/(oauth)/oauth/jwks.json/+server.ts | 0 .../group-chat}/src/routes/+layout.server.ts | 0 .../group-chat}/src/routes/+layout.svelte | 0 .../group-chat}/src/routes/+layout.ts | 0 .../group-chat}/src/routes/+page.server.ts | 0 .../group-chat}/src/routes/+page.svelte | 0 .../src/routes/api/blob-upload/+server.ts | 0 .../src/routes/api/blob/+server.ts | 0 .../src/routes/api/cron/+server.ts | 0 .../src/routes/api/ticket/+server.ts | 0 .../routes/c/[communityDid]/+layout.server.ts | 0 .../routes/c/[communityDid]/+layout.svelte | 0 .../src/routes/c/[communityDid]/+page.svelte | 0 .../[channelKey]/+page.server.ts | 0 .../[communityDid]/[channelKey]/+page.svelte | 0 .../settings/invites/+page.server.ts | 0 .../settings/invites/+page.svelte | 0 .../settings/members/+page.server.ts | 0 .../settings/members/+page.svelte | 0 .../src/routes/join/[token]/+page.svelte | 0 .../group-chat}/src/routes/new/+page.svelte | 0 .../src/routes/xrpc/[...path]/+server.ts | 0 .../group-chat}/static/favicon.png | Bin .../group-chat}/svelte.config.js | 0 .../group-chat}/tsconfig.json | 0 .../group-chat}/vite.config.ts | 0 .../group-chat}/wrangler.jsonc | 0 {examples => apps}/postgres/README.md | 0 {examples => apps}/postgres/config.ts | 0 .../postgres/docker-compose.yml | 0 {examples => apps}/postgres/ingest.ts | 0 {examples => apps}/postgres/package.json | 2 +- {examples => apps}/postgres/serve.ts | 0 {examples => apps}/postgres/sync.ts | 0 {examples => apps}/postgres/tsconfig.json | 0 Dockerfile => apps/rsvp-atmo/Dockerfile | 0 .../rsvp/atmo/community/adopt.json | 0 .../rsvp/atmo/community/defs.json | 0 .../rsvp/atmo/community/delete.json | 0 .../rsvp/atmo/community/deleteRecord.json | 0 .../rsvp/atmo/community/getHealth.json | 0 .../rsvp/atmo/community/invite/create.json | 0 .../rsvp/atmo/community/invite/list.json | 0 .../rsvp/atmo/community/invite/redeem.json | 0 .../rsvp/atmo/community/invite/revoke.json | 0 .../rsvp/atmo/community/list.json | 0 .../rsvp/atmo/community/mint.json | 0 .../rsvp/atmo/community/putRecord.json | 0 .../rsvp/atmo/community/reauth.json | 0 .../rsvp/atmo/community/space/create.json | 0 .../rsvp/atmo/community/space/delete.json | 0 .../atmo/community/space/deleteRecord.json | 0 .../rsvp/atmo/community/space/grant.json | 0 .../atmo/community/space/listMembers.json | 0 .../rsvp/atmo/community/space/putRecord.json | 0 .../rsvp/atmo/community/space/resync.json | 0 .../rsvp/atmo/community/space/revoke.json | 0 .../atmo/community/space/setAccessLevel.json | 0 .../rsvp/atmo/community/space/whoami.json | 0 .../rsvp/atmo/event/getRecord.json | 0 .../rsvp/atmo/event/listRecords.json | 0 .../rsvp/atmo/getCursor.json | 0 .../rsvp/atmo/getOverview.json | 0 .../rsvp/atmo/getProfile.json | 0 .../rsvp/atmo/notifyOfUpdate.json | 0 .../rsvp/atmo/permissionSet.json | 0 .../rsvp/atmo/rsvp/getRecord.json | 0 .../rsvp/atmo/rsvp/listRecords.json | 0 .../rsvp/atmo/space/addMember.json | 0 .../rsvp/atmo/space/createSpace.json | 0 .../rsvp/atmo/space/defs.json | 0 .../rsvp/atmo/space/deleteRecord.json | 0 .../rsvp/atmo/space/getBlob.json | 0 .../rsvp/atmo/space/getRecord.json | 0 .../rsvp/atmo/space/getSpace.json | 0 .../rsvp/atmo/space/leaveSpace.json | 0 .../rsvp/atmo/space/listBlobs.json | 0 .../rsvp/atmo/space/listMembers.json | 0 .../rsvp/atmo/space/listRecords.json | 0 .../rsvp/atmo/space/listSpaces.json | 0 .../rsvp/atmo/space/putRecord.json | 0 .../rsvp/atmo/space/removeMember.json | 0 .../rsvp/atmo/space/uploadBlob.json | 0 .../rsvp/atmo/spaceExt/invite/create.json | 0 .../rsvp/atmo/spaceExt/invite/list.json | 0 .../rsvp/atmo/spaceExt/invite/redeem.json | 0 .../rsvp/atmo/spaceExt/invite/revoke.json | 0 .../rsvp/atmo/spaceExt/whoami.json | 0 .../rsvp-atmo/lexicons-pulled}/README.md | 0 .../app/bsky/actor/profile.json | 0 .../community/lexicon/calendar/event.json | 0 .../community/lexicon/calendar/rsvp.json | 0 .../community/lexicon/location/address.json | 0 .../community/lexicon/location/fsq.json | 0 .../community/lexicon/location/geo.json | 0 .../community/lexicon/location/hthree.json | 0 .../rsvp-atmo/lexicons}/README.md | 0 apps/rsvp-atmo/package.json | 35 + {scripts => apps/rsvp-atmo/scripts}/clean.ts | 0 {scripts => apps/rsvp-atmo/scripts}/dev.ts | 0 .../rsvp-atmo/scripts}/publish-lexicons.ts | 2 +- {app => apps/rsvp-atmo/src}/config.ts | 2 +- {app => apps/rsvp-atmo/src}/generate.ts | 2 +- {app => apps/rsvp-atmo/src}/sync.ts | 2 +- {app => apps/rsvp-atmo/src}/worker.ts | 4 +- apps/rsvp-atmo/tsconfig.json | 7 + .../rsvp-atmo/wrangler.jsonc | 2 +- .../.claude/settings.local.json | 0 .../sveltekit-cloudflare-workers/.env.example | 0 .../sveltekit-cloudflare-workers}/.gitignore | 0 .../sveltekit-cloudflare-workers}/.npmrc | 0 .../.prettierignore | 0 .../sveltekit-cloudflare-workers}/.prettierrc | 0 .../sveltekit-cloudflare-workers}/LICENSE | 0 .../sveltekit-cloudflare-workers/README.md | 0 .../eslint.config.js | 0 .../statusphere/app/getCursor.json | 0 .../statusphere/app/getOverview.json | 0 .../statusphere/app/getProfile.json | 0 .../statusphere/app/notifyOfUpdate.json | 0 .../statusphere/app/permissionSet.json | 0 .../statusphere/app/status/getRecord.json | 0 .../statusphere/app/status/listRecords.json | 0 .../lexicons-pulled/README.md | 0 .../app/bsky/actor/profile.json | 0 .../xyz/statusphere/status.json | 0 .../sveltekit-cloudflare-workers/package.json | 3 +- .../scripts/append-scheduled.ts | 0 .../scripts/generate.ts | 2 +- .../scripts/sync.ts | 0 .../sveltekit-cloudflare-workers}/src/app.css | 0 .../sveltekit-cloudflare-workers/src/app.d.ts | 0 .../src/app.html | 0 .../src/hooks.server.ts | 0 .../src/lib/atproto/auth.svelte.ts | 0 .../src/lib/atproto/image-helper.ts | 0 .../src/lib/atproto/index.ts | 0 .../src/lib/atproto/methods.ts | 0 .../src/lib/atproto/port.ts | 0 .../src/lib/atproto/scripts/generate-key.ts | 0 .../lib/atproto/scripts/generate-secret.ts | 0 .../src/lib/atproto/scripts/setup-dev.ts | 0 .../src/lib/atproto/scripts/tunnel.ts | 0 .../src/lib/atproto/server/kv-store.ts | 0 .../src/lib/atproto/server/oauth.remote.ts | 0 .../src/lib/atproto/server/oauth.ts | 0 .../src/lib/atproto/server/profile.ts | 0 .../src/lib/atproto/server/repo.remote.ts | 0 .../src/lib/atproto/server/session.ts | 0 .../src/lib/atproto/server/signed-cookie.ts | 0 .../src/lib/atproto/settings.ts | 0 .../src/lib/atproto/ui/LoginModal.svelte | 0 .../src/lib/contrail/client.ts | 0 .../src/lib/contrail/config.ts | 0 .../src/lib/contrail/index.ts | 0 .../src/lib/emojis/icons.json | 0 .../src/lib/emojis/index.ts | 0 .../src/lib/index.ts | 0 .../oauth-client-metadata.json/+server.ts | 0 .../routes/(oauth)/oauth/callback/+server.ts | 0 .../routes/(oauth)/oauth/jwks.json/+server.ts | 0 .../src/routes/+layout.server.ts | 0 .../src/routes/+layout.svelte | 0 .../src/routes/+layout.ts | 0 .../src/routes/+page.server.ts | 0 .../src/routes/+page.svelte | 0 .../src/routes/api/cron/+server.ts | 0 .../src/routes/xrpc/[...path]/+server.ts | 0 .../static/favicon.png | Bin .../static/og.png | Bin .../svelte.config.js | 0 .../tsconfig.json | 0 .../vite.config.ts | 0 .../wrangler.jsonc | 0 bin/ingest.ts | 94 - examples/sveltekit-group-chat/pnpm-lock.yaml | 4750 ----------------- package.json | 112 +- .../contrail/CHANGELOG.md | 0 .../contrail/PERMISSIONED_DATA.md | 0 packages/contrail/README.md | 444 ++ packages/contrail/package.json | 77 + .../contrail/src}/adapters/postgres.ts | 0 .../contrail/src}/adapters/sqlite.ts | 0 {src => packages/contrail/src}/contrail.ts | 0 .../contrail/src}/core/backfill.ts | 0 {src => packages/contrail/src}/core/client.ts | 0 .../contrail/src}/core/community/acl.ts | 0 .../contrail/src}/core/community/adapter.ts | 0 .../src}/core/community/credentials.ts | 0 .../contrail/src}/core/community/index.ts | 0 .../contrail/src}/core/community/pds.ts | 0 .../contrail/src}/core/community/plc.ts | 0 .../contrail/src}/core/community/reconcile.ts | 0 .../contrail/src}/core/community/router.ts | 0 .../contrail/src}/core/community/schema.ts | 0 .../contrail/src}/core/community/types.ts | 0 .../contrail/src}/core/db/index.ts | 0 .../contrail/src}/core/db/records.ts | 0 .../contrail/src}/core/db/schema.ts | 0 .../contrail/src}/core/dialect.ts | 0 .../contrail/src}/core/identity.ts | 0 .../contrail/src}/core/invite/index.ts | 0 .../contrail/src}/core/invite/router.ts | 0 .../contrail/src}/core/invite/token.ts | 0 .../contrail/src}/core/jetstream.ts | 0 .../contrail/src}/core/persistent.ts | 0 .../src}/core/realtime/durable-object.ts | 0 .../contrail/src}/core/realtime/in-memory.ts | 0 .../contrail/src}/core/realtime/index.ts | 0 .../contrail/src}/core/realtime/merge.ts | 0 .../src}/core/realtime/publishing-adapter.ts | 0 .../src}/core/realtime/query-filter.ts | 0 .../contrail/src}/core/realtime/resolve.ts | 0 .../contrail/src}/core/realtime/router.ts | 0 .../contrail/src}/core/realtime/sse.ts | 0 .../contrail/src}/core/realtime/ticket.ts | 0 .../contrail/src}/core/realtime/types.ts | 0 .../contrail/src}/core/realtime/websocket.ts | 0 .../contrail/src}/core/router/admin.ts | 0 .../contrail/src}/core/router/collection.ts | 0 .../contrail/src}/core/router/feed.ts | 0 .../contrail/src}/core/router/helpers.ts | 0 .../contrail/src}/core/router/hydrate.ts | 0 .../contrail/src}/core/router/index.ts | 0 .../contrail/src}/core/router/notify.ts | 0 .../contrail/src}/core/router/profiles.ts | 0 {src => packages/contrail/src}/core/search.ts | 0 .../contrail/src}/core/spaces/acl.ts | 0 .../contrail/src}/core/spaces/adapter.ts | 0 .../contrail/src}/core/spaces/auth.ts | 0 .../contrail/src}/core/spaces/blob-adapter.ts | 0 .../contrail/src}/core/spaces/blob-gc.ts | 0 .../contrail/src}/core/spaces/blob-refs.ts | 0 .../contrail/src}/core/spaces/in-process.ts | 0 .../contrail/src}/core/spaces/router.ts | 0 .../contrail/src}/core/spaces/schema.ts | 0 .../contrail/src}/core/spaces/tid.ts | 0 .../contrail/src}/core/spaces/types.ts | 0 .../contrail/src}/core/spaces/uri.ts | 0 {src => packages/contrail/src}/core/types.ts | 0 {src => packages/contrail/src}/index.ts | 0 {src => packages/contrail/src}/server.ts | 0 .../tests}/community-delegation.test.ts | 0 .../contrail/tests}/community-e2e.test.ts | 0 .../contrail/tests}/community-mint.test.ts | 0 .../tests}/community-publishing.test.ts | 0 .../contrail/tests}/contrail-entry.test.ts | 0 .../contrail/tests}/dialect.test.ts | 0 .../contrail/tests}/helpers.test.ts | 0 {tests => packages/contrail/tests}/helpers.ts | 0 .../contrail/tests}/hydrate.test.ts | 0 .../contrail/tests}/invite-unified.test.ts | 0 .../contrail/tests}/notify.test.ts | 0 .../contrail/tests}/persistent.test.ts | 0 .../contrail/tests}/postgres-e2e.test.ts | 0 .../contrail/tests}/postgres.test.ts | 0 .../tests}/realtime-durable-object.test.ts | 0 .../contrail/tests}/realtime-e2e.test.ts | 0 .../tests}/realtime-foundations.test.ts | 0 .../contrail/tests}/realtime-public.test.ts | 0 .../contrail/tests}/records.test.ts | 0 .../contrail/tests}/schema.test.ts | 0 .../contrail/tests}/search.test.ts | 0 .../contrail/tests}/spaces-acl.test.ts | 0 .../contrail/tests}/spaces-blobs.test.ts | 0 .../contrail/tests}/spaces-e2e.test.ts | 0 .../contrail/tests}/spaces-invites.test.ts | 0 .../contrail/tests}/types.test.ts | 0 packages/contrail/tsconfig.build.json | 7 + packages/contrail/tsconfig.json | 8 + .../contrail/tsup.config.ts | 4 - .../contrail/vitest.config.ts | 0 packages/lexicons/README.md | 39 + .../lexicon-templates}/community/adopt.json | 0 .../lexicon-templates}/community/defs.json | 0 .../lexicon-templates}/community/delete.json | 0 .../community/deleteRecord.json | 0 .../community/getHealth.json | 0 .../lexicon-templates}/community/list.json | 0 .../lexicon-templates}/community/mint.json | 0 .../community/putRecord.json | 0 .../lexicon-templates}/community/reauth.json | 0 .../community/space/create.json | 0 .../community/space/delete.json | 0 .../community/space/deleteRecord.json | 0 .../community/space/grant.json | 0 .../community/space/listMembers.json | 0 .../community/space/putRecord.json | 0 .../community/space/resync.json | 0 .../community/space/revoke.json | 0 .../community/space/setAccessLevel.json | 0 .../lexicon-templates}/invite/create.json | 0 .../lexicon-templates}/invite/defs.json | 0 .../lexicon-templates}/invite/list.json | 0 .../lexicon-templates}/invite/redeem.json | 0 .../lexicon-templates}/invite/revoke.json | 0 .../realtime/subscribe.json | 0 .../lexicon-templates}/realtime/ticket.json | 0 .../lexicon-templates}/spaces/addMember.json | 0 .../spaces/createSpace.json | 0 .../lexicon-templates}/spaces/defs.json | 0 .../spaces/deleteRecord.json | 0 .../lexicon-templates}/spaces/ext/whoami.json | 0 .../lexicon-templates}/spaces/getBlob.json | 0 .../lexicon-templates}/spaces/getRecord.json | 0 .../lexicon-templates}/spaces/getSpace.json | 0 .../lexicon-templates}/spaces/leaveSpace.json | 0 .../lexicon-templates}/spaces/listBlobs.json | 0 .../spaces/listMembers.json | 0 .../spaces/listRecords.json | 0 .../lexicon-templates}/spaces/listSpaces.json | 0 .../lexicon-templates}/spaces/putRecord.json | 0 .../spaces/removeMember.json | 0 .../lexicon-templates}/spaces/uploadBlob.json | 0 packages/lexicons/package.json | 59 + packages/lexicons/src/cli.ts | 172 + {src => packages/lexicons/src}/generate.ts | 11 +- packages/lexicons/src/index.ts | 9 + {src => packages/lexicons/src}/publish.ts | 0 .../lexicons/tests}/generate.test.ts | 2 +- packages/lexicons/tsconfig.build.json | 7 + packages/lexicons/tsconfig.json | 7 + packages/lexicons/tsup.config.ts | 15 + packages/lexicons/vitest.config.ts | 7 + packages/sync/README.md | 28 + packages/sync/package.json | 42 + {src/sync => packages/sync/src}/cache-idb.ts | 0 {src/sync => packages/sync/src}/index.ts | 0 packages/sync/tsconfig.build.json | 7 + packages/sync/tsconfig.json | 7 + packages/sync/tsup.config.ts | 10 + pnpm-lock.yaml | 1937 +++---- pnpm-workspace.yaml | 7 +- tsconfig.build.json => tsconfig.base.json | 7 +- tsconfig.json | 17 - turbo.json | 25 + 465 files changed, 2200 insertions(+), 6319 deletions(-) create mode 100644 .github/workflows/ci.yml rename {examples => apps}/cloudflare-workers/.gitignore (100%) rename {examples => apps}/cloudflare-workers/README.md (100%) rename {examples => apps}/cloudflare-workers/config.ts (100%) rename {examples => apps}/cloudflare-workers/generate.ts (82%) rename {examples => apps}/cloudflare-workers/package.json (88%) rename {examples => apps}/cloudflare-workers/sync.ts (100%) rename {examples => apps}/cloudflare-workers/tsconfig.json (100%) rename {examples => apps}/cloudflare-workers/worker.ts (100%) rename {examples => apps}/cloudflare-workers/wrangler.jsonc (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/.claude/settings.local.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/.env.example (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/.gitignore (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/.npmrc (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/.prettierignore (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/.prettierrc (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/LICENSE (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/README.md (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/eslint.config.js (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/channel/getRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/channel/listRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/channel/watchRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/adopt.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/defs.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/delete.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/deleteRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/getHealth.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/list.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/mint.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/putRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/reauth.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/create.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/delete.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/grant.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/listMembers.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/putRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/resync.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/revoke.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/getCursor.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/getOverview.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/getProfile.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/invite/create.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/invite/defs.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/invite/list.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/invite/redeem.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/invite/revoke.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/message/getRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/message/listRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/message/watchRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/permissionSet.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/realtime/subscribe.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/realtime/ticket.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/server/getRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/server/listRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/server/watchRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/addMember.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/createSpace.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/defs.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/deleteRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/getBlob.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/getRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/getSpace.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/leaveSpace.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/listBlobs.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/listMembers.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/listRecords.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/listSpaces.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/putRecord.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/removeMember.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/space/uploadBlob.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons-pulled/README.md (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/lexicons-pulled/app/bsky/actor/profile.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons/tools/atmo/chat/channel.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons/tools/atmo/chat/message.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/lexicons/tools/atmo/chat/server.json (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/package.json (96%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/pnpm-lock.yaml (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/scripts/append-scheduled.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/scripts/generate.ts (98%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/scripts/sync.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/app.css (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/app.d.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/app.html (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/hooks.server.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/auth.svelte.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/atproto/generated-methods.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/image-helper.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/index.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/methods.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/port.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/scripts/generate-key.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/scripts/generate-secret.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/scripts/setup-dev.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/scripts/tunnel.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/server/kv-store.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/server/oauth.remote.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/server/oauth.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/server/profile.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/atproto/server/repo.remote.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/server/session.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/server/signed-cookie.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/atproto/settings.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/atproto/ui/LoginModal.svelte (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/contrail/client.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/contrail/config.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/contrail/index.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/emojis/icons.json (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/emojis/index.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/lib/index.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/channels-context.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/connection.svelte.ts (94%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/invites.remote.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/profiles.svelte.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/realtime.svelte.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/rooms.remote.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/unread.svelte.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/uri.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/lib/rooms/watch.svelte.ts (98%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/routes/(oauth)/oauth-client-metadata.json/+server.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/routes/(oauth)/oauth/callback/+server.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/routes/(oauth)/oauth/jwks.json/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/+layout.server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/+layout.svelte (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/src/routes/+layout.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/+page.server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/api/blob-upload/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/api/blob/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/api/cron/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/api/ticket/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/+layout.server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/+layout.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/[channelKey]/+page.server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/[channelKey]/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/settings/invites/+page.server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/settings/invites/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/settings/members/+page.server.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/c/[communityDid]/settings/members/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/join/[token]/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/new/+page.svelte (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/src/routes/xrpc/[...path]/+server.ts (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/static/favicon.png (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/svelte.config.js (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/tsconfig.json (100%) rename {examples/sveltekit-cloudflare-workers => apps/group-chat}/vite.config.ts (100%) rename {examples/sveltekit-group-chat => apps/group-chat}/wrangler.jsonc (100%) rename {examples => apps}/postgres/README.md (100%) rename {examples => apps}/postgres/config.ts (100%) rename {examples => apps}/postgres/docker-compose.yml (100%) rename {examples => apps}/postgres/ingest.ts (100%) rename {examples => apps}/postgres/package.json (89%) rename {examples => apps}/postgres/serve.ts (100%) rename {examples => apps}/postgres/sync.ts (100%) rename {examples => apps}/postgres/tsconfig.json (100%) rename Dockerfile => apps/rsvp-atmo/Dockerfile (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/adopt.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/defs.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/delete.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/deleteRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/getHealth.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/invite/create.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/invite/list.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/invite/redeem.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/invite/revoke.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/list.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/mint.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/putRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/reauth.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/create.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/delete.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/deleteRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/grant.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/listMembers.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/putRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/resync.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/revoke.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/setAccessLevel.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/community/space/whoami.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/event/getRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/event/listRecords.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/getCursor.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/getOverview.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/getProfile.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/notifyOfUpdate.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/permissionSet.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/rsvp/getRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/rsvp/listRecords.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/addMember.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/createSpace.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/defs.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/deleteRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/getBlob.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/getRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/getSpace.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/leaveSpace.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/listBlobs.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/listMembers.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/listRecords.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/listSpaces.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/putRecord.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/removeMember.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/space/uploadBlob.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/spaceExt/invite/create.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/spaceExt/invite/list.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/spaceExt/invite/redeem.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/spaceExt/invite/revoke.json (100%) rename {lexicons-generated => apps/rsvp-atmo/lexicons-generated}/rsvp/atmo/spaceExt/whoami.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/README.md (100%) rename {examples/sveltekit-group-chat => apps/rsvp-atmo}/lexicons-pulled/app/bsky/actor/profile.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/community/lexicon/calendar/event.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/community/lexicon/calendar/rsvp.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/community/lexicon/location/address.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/community/lexicon/location/fsq.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/community/lexicon/location/geo.json (100%) rename {lexicons-pulled => apps/rsvp-atmo/lexicons-pulled}/community/lexicon/location/hthree.json (100%) rename {lexicons => apps/rsvp-atmo/lexicons}/README.md (100%) create mode 100644 apps/rsvp-atmo/package.json rename {scripts => apps/rsvp-atmo/scripts}/clean.ts (100%) rename {scripts => apps/rsvp-atmo/scripts}/dev.ts (100%) rename {scripts => apps/rsvp-atmo/scripts}/publish-lexicons.ts (94%) rename {app => apps/rsvp-atmo/src}/config.ts (96%) rename {app => apps/rsvp-atmo/src}/generate.ts (86%) rename {app => apps/rsvp-atmo/src}/sync.ts (97%) rename {app => apps/rsvp-atmo/src}/worker.ts (85%) create mode 100644 apps/rsvp-atmo/tsconfig.json rename wrangler.jsonc => apps/rsvp-atmo/wrangler.jsonc (93%) rename {examples => apps}/sveltekit-cloudflare-workers/.claude/settings.local.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/.env.example (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/.gitignore (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/.npmrc (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/.prettierignore (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/.prettierrc (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/LICENSE (100%) rename {examples => apps}/sveltekit-cloudflare-workers/README.md (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/eslint.config.js (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-pulled/README.md (100%) rename {lexicons-pulled => apps/sveltekit-cloudflare-workers/lexicons-pulled}/app/bsky/actor/profile.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json (100%) rename {examples => apps}/sveltekit-cloudflare-workers/package.json (96%) rename {examples => apps}/sveltekit-cloudflare-workers/scripts/append-scheduled.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/scripts/generate.ts (83%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/scripts/sync.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/app.css (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/app.d.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/app.html (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/hooks.server.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/auth.svelte.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/image-helper.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/index.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/methods.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/port.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/scripts/generate-key.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/scripts/generate-secret.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/scripts/setup-dev.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/scripts/tunnel.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/server/kv-store.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/server/oauth.remote.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/server/oauth.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/server/profile.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/lib/atproto/server/repo.remote.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/server/session.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/server/signed-cookie.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/lib/atproto/settings.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/atproto/ui/LoginModal.svelte (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/contrail/client.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/lib/contrail/config.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/lib/contrail/index.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/emojis/icons.json (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/emojis/index.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/lib/index.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/routes/(oauth)/oauth-client-metadata.json/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/routes/(oauth)/oauth/callback/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/routes/(oauth)/oauth/jwks.json/+server.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/routes/+layout.server.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/routes/+layout.svelte (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/src/routes/+layout.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/routes/+page.server.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/routes/+page.svelte (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/routes/api/cron/+server.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/src/routes/xrpc/[...path]/+server.ts (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/static/favicon.png (100%) rename {examples => apps}/sveltekit-cloudflare-workers/static/og.png (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/svelte.config.js (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/tsconfig.json (100%) rename {examples/sveltekit-group-chat => apps/sveltekit-cloudflare-workers}/vite.config.ts (100%) rename {examples => apps}/sveltekit-cloudflare-workers/wrangler.jsonc (100%) delete mode 100644 bin/ingest.ts delete mode 100644 examples/sveltekit-group-chat/pnpm-lock.yaml rename CHANGELOG.md => packages/contrail/CHANGELOG.md (100%) rename PERMISSIONED_DATA.md => packages/contrail/PERMISSIONED_DATA.md (100%) create mode 100644 packages/contrail/README.md create mode 100644 packages/contrail/package.json rename {src => packages/contrail/src}/adapters/postgres.ts (100%) rename {src => packages/contrail/src}/adapters/sqlite.ts (100%) rename {src => packages/contrail/src}/contrail.ts (100%) rename {src => packages/contrail/src}/core/backfill.ts (100%) rename {src => packages/contrail/src}/core/client.ts (100%) rename {src => packages/contrail/src}/core/community/acl.ts (100%) rename {src => packages/contrail/src}/core/community/adapter.ts (100%) rename {src => packages/contrail/src}/core/community/credentials.ts (100%) rename {src => packages/contrail/src}/core/community/index.ts (100%) rename {src => packages/contrail/src}/core/community/pds.ts (100%) rename {src => packages/contrail/src}/core/community/plc.ts (100%) rename {src => packages/contrail/src}/core/community/reconcile.ts (100%) rename {src => packages/contrail/src}/core/community/router.ts (100%) rename {src => packages/contrail/src}/core/community/schema.ts (100%) rename {src => packages/contrail/src}/core/community/types.ts (100%) rename {src => packages/contrail/src}/core/db/index.ts (100%) rename {src => packages/contrail/src}/core/db/records.ts (100%) rename {src => packages/contrail/src}/core/db/schema.ts (100%) rename {src => packages/contrail/src}/core/dialect.ts (100%) rename {src => packages/contrail/src}/core/identity.ts (100%) rename {src => packages/contrail/src}/core/invite/index.ts (100%) rename {src => packages/contrail/src}/core/invite/router.ts (100%) rename {src => packages/contrail/src}/core/invite/token.ts (100%) rename {src => packages/contrail/src}/core/jetstream.ts (100%) rename {src => packages/contrail/src}/core/persistent.ts (100%) rename {src => packages/contrail/src}/core/realtime/durable-object.ts (100%) rename {src => packages/contrail/src}/core/realtime/in-memory.ts (100%) rename {src => packages/contrail/src}/core/realtime/index.ts (100%) rename {src => packages/contrail/src}/core/realtime/merge.ts (100%) rename {src => packages/contrail/src}/core/realtime/publishing-adapter.ts (100%) rename {src => packages/contrail/src}/core/realtime/query-filter.ts (100%) rename {src => packages/contrail/src}/core/realtime/resolve.ts (100%) rename {src => packages/contrail/src}/core/realtime/router.ts (100%) rename {src => packages/contrail/src}/core/realtime/sse.ts (100%) rename {src => packages/contrail/src}/core/realtime/ticket.ts (100%) rename {src => packages/contrail/src}/core/realtime/types.ts (100%) rename {src => packages/contrail/src}/core/realtime/websocket.ts (100%) rename {src => packages/contrail/src}/core/router/admin.ts (100%) rename {src => packages/contrail/src}/core/router/collection.ts (100%) rename {src => packages/contrail/src}/core/router/feed.ts (100%) rename {src => packages/contrail/src}/core/router/helpers.ts (100%) rename {src => packages/contrail/src}/core/router/hydrate.ts (100%) rename {src => packages/contrail/src}/core/router/index.ts (100%) rename {src => packages/contrail/src}/core/router/notify.ts (100%) rename {src => packages/contrail/src}/core/router/profiles.ts (100%) rename {src => packages/contrail/src}/core/search.ts (100%) rename {src => packages/contrail/src}/core/spaces/acl.ts (100%) rename {src => packages/contrail/src}/core/spaces/adapter.ts (100%) rename {src => packages/contrail/src}/core/spaces/auth.ts (100%) rename {src => packages/contrail/src}/core/spaces/blob-adapter.ts (100%) rename {src => packages/contrail/src}/core/spaces/blob-gc.ts (100%) rename {src => packages/contrail/src}/core/spaces/blob-refs.ts (100%) rename {src => packages/contrail/src}/core/spaces/in-process.ts (100%) rename {src => packages/contrail/src}/core/spaces/router.ts (100%) rename {src => packages/contrail/src}/core/spaces/schema.ts (100%) rename {src => packages/contrail/src}/core/spaces/tid.ts (100%) rename {src => packages/contrail/src}/core/spaces/types.ts (100%) rename {src => packages/contrail/src}/core/spaces/uri.ts (100%) rename {src => packages/contrail/src}/core/types.ts (100%) rename {src => packages/contrail/src}/index.ts (100%) rename {src => packages/contrail/src}/server.ts (100%) rename {tests => packages/contrail/tests}/community-delegation.test.ts (100%) rename {tests => packages/contrail/tests}/community-e2e.test.ts (100%) rename {tests => packages/contrail/tests}/community-mint.test.ts (100%) rename {tests => packages/contrail/tests}/community-publishing.test.ts (100%) rename {tests => packages/contrail/tests}/contrail-entry.test.ts (100%) rename {tests => packages/contrail/tests}/dialect.test.ts (100%) rename {tests => packages/contrail/tests}/helpers.test.ts (100%) rename {tests => packages/contrail/tests}/helpers.ts (100%) rename {tests => packages/contrail/tests}/hydrate.test.ts (100%) rename {tests => packages/contrail/tests}/invite-unified.test.ts (100%) rename {tests => packages/contrail/tests}/notify.test.ts (100%) rename {tests => packages/contrail/tests}/persistent.test.ts (100%) rename {tests => packages/contrail/tests}/postgres-e2e.test.ts (100%) rename {tests => packages/contrail/tests}/postgres.test.ts (100%) rename {tests => packages/contrail/tests}/realtime-durable-object.test.ts (100%) rename {tests => packages/contrail/tests}/realtime-e2e.test.ts (100%) rename {tests => packages/contrail/tests}/realtime-foundations.test.ts (100%) rename {tests => packages/contrail/tests}/realtime-public.test.ts (100%) rename {tests => packages/contrail/tests}/records.test.ts (100%) rename {tests => packages/contrail/tests}/schema.test.ts (100%) rename {tests => packages/contrail/tests}/search.test.ts (100%) rename {tests => packages/contrail/tests}/spaces-acl.test.ts (100%) rename {tests => packages/contrail/tests}/spaces-blobs.test.ts (100%) rename {tests => packages/contrail/tests}/spaces-e2e.test.ts (100%) rename {tests => packages/contrail/tests}/spaces-invites.test.ts (100%) rename {tests => packages/contrail/tests}/types.test.ts (100%) create mode 100644 packages/contrail/tsconfig.build.json create mode 100644 packages/contrail/tsconfig.json rename tsup.config.ts => packages/contrail/tsup.config.ts (76%) rename vitest.config.ts => packages/contrail/vitest.config.ts (100%) create mode 100644 packages/lexicons/README.md rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/adopt.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/defs.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/delete.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/deleteRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/getHealth.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/list.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/mint.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/putRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/reauth.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/create.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/delete.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/deleteRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/grant.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/listMembers.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/putRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/resync.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/revoke.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/community/space/setAccessLevel.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/invite/create.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/invite/defs.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/invite/list.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/invite/redeem.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/invite/revoke.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/realtime/subscribe.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/realtime/ticket.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/addMember.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/createSpace.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/defs.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/deleteRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/ext/whoami.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/getBlob.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/getRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/getSpace.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/leaveSpace.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/listBlobs.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/listMembers.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/listRecords.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/listSpaces.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/putRecord.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/removeMember.json (100%) rename {lexicon-templates => packages/lexicons/lexicon-templates}/spaces/uploadBlob.json (100%) create mode 100644 packages/lexicons/package.json create mode 100644 packages/lexicons/src/cli.ts rename {src => packages/lexicons/src}/generate.ts (98%) create mode 100644 packages/lexicons/src/index.ts rename {src => packages/lexicons/src}/publish.ts (100%) rename {tests => packages/lexicons/tests}/generate.test.ts (99%) create mode 100644 packages/lexicons/tsconfig.build.json create mode 100644 packages/lexicons/tsconfig.json create mode 100644 packages/lexicons/tsup.config.ts create mode 100644 packages/lexicons/vitest.config.ts create mode 100644 packages/sync/README.md create mode 100644 packages/sync/package.json rename {src/sync => packages/sync/src}/cache-idb.ts (100%) rename {src/sync => packages/sync/src}/index.ts (100%) create mode 100644 packages/sync/tsconfig.build.json create mode 100644 packages/sync/tsconfig.json create mode 100644 packages/sync/tsup.config.ts rename tsconfig.build.json => tsconfig.base.json (72%) delete mode 100644 tsconfig.json create mode 100644 turbo.json diff --git a/.changeset/config.json b/.changeset/config.json index 2be13d4..9166825 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -3,7 +3,7 @@ "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], - "linked": [], + "linked": [["@atmo-dev/contrail", "@atmo-dev/contrail-sync"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..af8aae8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + pull_request: + push: + branches-ignore: ['main'] + +jobs: + check: + name: Build, test, typecheck + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - uses: pnpm/action-setup@v4 + with: + version: 10.30.1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.14 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfcc204..c99c117 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: branches: ['main'] permissions: - id-token: write + id-token: write # OIDC for npm trusted publishing contents: write pull-requests: write @@ -18,6 +18,16 @@ jobs: steps: - name: Check out code uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- - uses: pnpm/action-setup@v4 with: @@ -34,7 +44,7 @@ jobs: run: npm install -g npm@11.5.1 - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Build run: pnpm build diff --git a/.gitignore b/.gitignore index d17a0b7..09e8139 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,12 @@ node_modules .wrangler dist +# Turbo +.turbo + # Generated (internal only) -lex.config.js -src/lexicon-types/ -src/core/queryable.generated.ts +packages/contrail/src/core/queryable.generated.ts +apps/*/src/lexicon-types/ +apps/*/lex.config.js -stuff/ \ No newline at end of file +stuff/ diff --git a/README.md b/README.md index 4e95bd6..0f711a7 100644 --- a/README.md +++ b/README.md @@ -1,444 +1,51 @@ # Contrail -> [!WARNING] -> Work in progress! Pre-alpha, expect breaking changes. +> **Work in progress!** Pre-alpha, expect breaking changes. -A library for indexing AT Protocol records. Define collections — get automatic Jetstream ingestion, PDS backfill, user discovery, typed XRPC endpoints, and (opt-in) permissioned spaces for private records. Works with Cloudflare Workers + D1, SvelteKit, Node.js, or any JavaScript runtime. +Contrail is a library (and a small set of sibling packages) for building AT Protocol appviews. Define collections, get automatic Jetstream ingestion, PDS backfill, typed XRPC endpoints, permissioned spaces for private records, group-controlled communities, and a client-side reactive sync layer. -## Install +## Packages -```bash -npm install @atmo-dev/contrail -``` - -## Usage - -```ts -import { Contrail } from "@atmo-dev/contrail"; - -const contrail = new Contrail({ - namespace: "com.example", - db, // any Database-compatible instance (D1, SQLite, etc.) - collections: { - event: { // short name → URL path + table suffix - collection: "community.lexicon.calendar.event", // full NSID of the record type - queryable: { - mode: {}, // string → equality filter (?mode=online) - name: {}, // string → equality filter (?name=...) - startsAt: { type: "range" }, // range → min/max filters (?startsAtMin=...&startsAtMax=...) - endsAt: { type: "range" }, - }, - searchable: ["name", "description"], - relations: { - rsvps: { - collection: "rsvp", // short name of the child collection - groupBy: "status", - count: true, - groups: { - interested: "community.lexicon.calendar.rsvp#interested", - going: "community.lexicon.calendar.rsvp#going", - notgoing: "community.lexicon.calendar.rsvp#notgoing", - }, - }, - }, - }, - rsvp: { - collection: "community.lexicon.calendar.rsvp", - queryable: { - status: {}, - "subject.uri": {}, - }, - references: { - event: { - collection: "event", // short name of the referenced collection - field: "subject.uri", - }, - }, - }, - }, -}); - -await contrail.init(); -``` - -### Query records - -```ts -const { records, cursor } = await contrail.query( - "event", // short name you declared in `collections` - { - filters: { mode: "in-person" }, - sort: { countType: "rsvp", direction: "desc" }, - limit: 20, - } -); -``` - -### Ingest from Jetstream - -```ts -// Run one ingestion cycle (catches up to present, then stops) -await contrail.ingest(); -``` - -### Persistent ingestion - -```ts -// Long-lived Jetstream connection with automatic batching and reconnection -const controller = new AbortController(); -await contrail.runPersistent({ - batchSize: 50, // flush every N events (default: 50) - flushIntervalMs: 5000, // or every N ms (default: 5000) - signal: controller.signal, -}); -``` +| Package | Description | +|---|---| +| [`@atmo-dev/contrail`](./packages/contrail) | Core library — indexing, XRPC server, spaces, communities, realtime publishing. | +| [`@atmo-dev/contrail-sync`](./packages/sync) | Client-side reactive watch-store over `watchRecords`. SSE + WebSocket, IndexedDB cache. | +| [`@atmo-dev/contrail-lexicons`](./packages/lexicons) | Lexicon codegen from a Contrail config + CLI (`contrail-lex`) wrapping `@atcute/lex-cli`. | -Call `controller.abort()` for graceful shutdown — the current batch is flushed and the cursor is saved. +## Apps (reference deployments — `workspace:*`-linked) -### Discover users and backfill +| App | Description | +|---|---| +| [`rsvp-atmo`](./apps/rsvp-atmo) | Cloudflare Workers + D1 indexer for `community.lexicon.calendar.*`. | +| [`group-chat`](./apps/group-chat) | Full-featured SvelteKit + Workers group chat using spaces, communities, and realtime. | +| [`postgres`](./apps/postgres) | Node + PostgreSQL minimal indexer. | +| [`cloudflare-workers`](./apps/cloudflare-workers) | Minimal Worker example. | +| [`sveltekit-cloudflare-workers`](./apps/sveltekit-cloudflare-workers) | SvelteKit Statusphere-style example. | -```ts -// Find users from relays -await contrail.discover(); - -// Backfill their records from PDS -await contrail.backfill({ concurrency: 100 }); - -// Or both in one call -await contrail.sync({ concurrency: 100 }); -``` - -### Notify of immediate updates - -```ts -// After writing to a user's PDS, tell Contrail to fetch it now -await contrail.notify("at://did:plc:abc/community.lexicon.calendar.rsvp/123"); - -// Batch up to 25 URIs -await contrail.notify([uri1, uri2, uri3]); -``` - -### HTTP handler (XRPC endpoints) - -Mount the full XRPC API in any framework: - -```ts -import { createHandler } from "@atmo-dev/contrail/server"; - -const handle = createHandler(contrail); -// handle: (Request, db?) => Promise -``` - -**SvelteKit:** - -```ts -// src/routes/xrpc/[...path]/+server.ts -export const GET = ({ request }) => handle(request); -export const POST = ({ request }) => handle(request); -``` - -**Cloudflare Worker:** - -```ts -export default { - async fetch(request, env) { - return handle(request, env.DB); - }, -}; -``` - -### SQLite adapter (Node.js / local dev) - -```ts -import { createSqliteDatabase } from "@atmo-dev/contrail/sqlite"; - -const db = createSqliteDatabase("data.db"); -const contrail = new Contrail({ ...config, db }); -``` - -> **Note:** The SQLite adapter uses Node's built-in `node:sqlite` (Node 22+). Full-text search (`searchable`) is not supported with this adapter because `node:sqlite` doesn't include the FTS5 extension. Search works on Cloudflare D1 and PostgreSQL. - -### PostgreSQL adapter (Node.js / server) - -```ts -import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; -import pg from "pg"; - -const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); -const db = createPostgresDatabase(pool); -const contrail = new Contrail({ ...config, db }); -``` - -PostgreSQL uses JSONB for record storage, tsvector generated columns for full-text search (instead of FTS5), and `BIGINT` for timestamp columns. - -## Examples - -### PostgreSQL (Node.js) - -See [`examples/postgres/`](examples/postgres/) for a complete example with Docker Compose, persistent Jetstream ingestion, user discovery/backfill, and an HTTP API server. - -### Cloudflare Workers - -This repo includes a working example that indexes AT Protocol calendar events and RSVPs on Cloudflare Workers + D1. - -### Setup +## Setup ```bash pnpm install -pnpm generate:pull # pull lexicons from network, auto-detect fields, generate types -``` - -### Dev - -```bash -pnpm sync # discover users and backfill records from PDS -pnpm dev:auto # start wrangler dev with auto-ingestion +pnpm build +pnpm test ``` -### Production +Per-package commands run through Turbo: ```bash -npx wrangler d1 create contrail -# Add database_id to wrangler.jsonc -pnpm deploy -pnpm sync # discover + backfill against prod D1 +pnpm build # turbo run build +pnpm typecheck # turbo run typecheck +pnpm test # turbo run test +pnpm --filter @atmo-dev/contrail build +pnpm --filter rsvp-atmo dev ``` -Ingestion runs automatically via cron (`*/1 * * * *`). Schema is auto-initialized. - -## Config - -### Collection options - -| Option | Default | Description | -|--------|---------|-------------| -| `queryable` | `{}` | Fields exposed as query filters. `{}` = string equality, `{ type: "range" }` = min/max | -| `discover` | `true` | Find users via relays. `false` = only track known DIDs | -| `relations` | `{}` | Many-to-one relationships with materialized counts | -| `relations.*.field` | `"subject.uri"` | Field in the related record to match against | -| `relations.*.match` | `"uri"` | Match against parent's `"uri"` or `"did"` | -| `relations.*.groupBy` | — | Split counts by this field's value | -| `relations.*.groups` | — | Group value mappings (e.g. `{ going: "collection#going" }`) | -| `relations.*.count` | `true` | Enable materialized count columns on the parent | -| `references` | `{}` | Forward references to other collections for hydration | -| `references.*.collection` | — | Short name of the target collection (key in `collections`) | -| `references.*.field` | — | Field containing the target record's AT URI | -| `queries` | `{}` | Custom query handlers (raw Response) | -| `pipelineQueries` | `{}` | Custom query handlers that go through the standard filter/sort/hydration pipeline | -| `searchable` | disabled | Full-text search fields. SQLite uses FTS5 virtual tables; PostgreSQL uses tsvector generated columns with GIN indexes. Provide `string[]` to enable, omit to disable | -| `collection` | — | Full NSID of the record type this short-name entry indexes (required) | -| `methods` | `["listRecords", "getRecord"]` | XRPC methods to emit for this collection | -| `allowInSpaces` | `true` | When spaces are enabled, emit a parallel `spaces_records_` table | - -### Top-level options - -| Option | Default | Description | -|--------|---------|-------------| -| `namespace` | — | Your reverse-domain namespace (e.g. `"com.example"`) | -| `collections` | — | Collection configurations | -| `profiles` | `["app.bsky.actor.profile"]` | Profile collection NSIDs | -| `relays` | Bluesky relays | Relay URLs for user discovery | -| `jetstreams` | Bluesky Jetstream | Jetstream URLs for real-time ingestion | -| `feeds` | — | Personalized feed configurations | -| `notify` | off | Expose `notifyOfUpdate`. `true` = open, string = `Authorization: Bearer ` required | -| `spaces` | — | Permissioned-spaces configuration. See [PERMISSIONED_DATA.md](./PERMISSIONED_DATA.md) | -| `logger` | `console` | Logger instance (`{ log, warn, error }`) | - -### Profiles - -`profiles` is a top-level config array of collection NSIDs that contain profile records (rkey `self`). Defaults to `["app.bsky.actor.profile"]`. These are auto-added to `collections` with `{ discover: false }`. Use `?profiles=true` on any endpoint to include a `profiles` map in the response, keyed by DID, with handle and profile record data. +## Releasing -## XRPC API - -When using `createHandler`, all endpoints live under the deployment's own namespace at `/xrpc/{namespace}.{...}`: - -| Endpoint | Description | -|----------|-------------| -| `{namespace}.{short}.listRecords` | List/filter records in a collection (keyed by its short name) | -| `{namespace}.{short}.getRecord` | Get single record by URI | -| `{namespace}.getProfile` | Get a user's profile by DID or handle | -| `{namespace}.notifyOfUpdate` | Notify of a record change for immediate indexing | -| `{namespace}.getCursor` | Current cursor position | -| `{namespace}.getOverview` | All collections summary | -| `{namespace}.permissionSet` | OAuth permission-set bundling every method above (auto-generated) | -| `{namespace}.space.*` | Spaces admin, invite, member, record XRPCs (when `spaces` is enabled) | - -### Query parameters - -**Filtering:** - -| Param | Example | Description | -|-------|---------|-------------| -| `actor` | `?actor=did:plc:...` or `?actor=alice.bsky.social` | Filter by DID or handle (triggers on-demand backfill) | -| `profiles` | `?profiles=true` | Include profile + identity info keyed by DID | -| `search` | `?search=meetup` | Full-text search across searchable fields (FTS5, ranked) | -| `{field}` | `?status=going` | Equality filter on queryable string field | -| `{field}Min` | `?startsAtMin=2026-03-16` | Range minimum (datetime/integer fields) | -| `{field}Max` | `?endsAtMax=2026-04-01` | Range maximum (datetime/integer fields) | -| `{rel}CountMin` | `?rsvpsCountMin=10` | Minimum total relation count | -| `{rel}{Group}CountMin` | `?rsvpsGoingCountMin=10` | Minimum relation count for a specific groupBy value | -| `hydrate{Rel}` | `?hydrateRsvps=10` | Embed latest N related records (per group if grouped) | -| `hydrate{Ref}` | `?hydrateEvent=true` | Embed the referenced record | -| `sort` | `?sort=startsAt` | Sort by a queryable field or count (see below) | -| `order` | `?order=asc` | Sort direction: `asc` or `desc` (default depends on field type) | -| `limit` | `?limit=25` | Page size (1-200, default 50) | -| `cursor` | `?cursor=...` | Pagination cursor | - -**Sorting** — `sort` accepts any queryable field param name or a count field: - -``` -?sort=startsAt # by date (default: desc for range fields) -?sort=name&order=asc # by name ascending -?sort=rsvpsCount # by total RSVP count (default: desc) -?sort=rsvpsGoingCount&order=asc # by going count ascending -``` - -**Search** uses SQLite FTS5 or PostgreSQL tsvector for ranked full-text search. To enable, set `searchable: ["field1", "field2"]` on a collection. Supports FTS5 syntax including prefix (`meetup*`), phrases (`"rust meetup"`), and boolean (`rust OR typescript`). Combinable with all other filters. - -``` -?search=meetup # basic search -?search=meetup&mode=online # search + filter -?search=rust*&sort=startsAt&order=asc # search + sort override -``` - -**Hydration** embeds related or referenced records inline: - -``` -?hydrateRsvps=5 # latest 5 RSVPs per group (going, interested, etc.) -?hydrateEvent=true # embed the referenced event record -?hydrateRsvps=5&hydrateEvent=true # combine both -``` - -### Examples (events) - -``` -# Upcoming events with 10+ going RSVPs, with RSVP records and profiles -/xrpc/com.example.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10&hydrateRsvps=5&profiles=true - -# Events for a specific user (by handle) -/xrpc/com.example.event.listRecords?actor=alice.bsky.social&profiles=true - -# Single event with counts, RSVPs, and profiles -/xrpc/com.example.event.getRecord?uri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateRsvps=10&profiles=true - -# Search for events by name/description -/xrpc/com.example.event.listRecords?search=meetup&profiles=true - -# RSVPs for a specific event, with the referenced event embedded -/xrpc/com.example.rsvp.listRecords?subjectUri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateEvent=true&profiles=true -``` - -## Notify of Updates - -By default, Contrail ingests from Jetstream every minute (in the Worker example). If your app writes to a user's PDS and needs the change reflected immediately, use `contrail.notify()` or call the XRPC endpoint: - -```ts -// Programmatic -await contrail.notify(uri); - -// Or via HTTP -await fetch("https://your-contrail.workers.dev/xrpc/com.example.notifyOfUpdate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ uri }), -}); -``` - -Contrail fetches the record from the user's PDS and figures out what to do: - -| PDS returns | Already indexed? | Action | -|---|---|---| -| Record (new CID) | No | **Create** — indexes it, recounts relations | -| Record (new CID) | Yes | **Update** — upserts the record, recounts relations | -| Record (same CID) | Yes | **Skip** — nothing changed | -| 404 | Yes | **Delete** — removes it, recounts relations | -| 404 | No | **No-op** | - -When Jetstream later delivers the same event, the duplicate is detected by CID and skipped. - -## Permissioned Data - -Contrail ships an opt-in permissioned-spaces subsystem: an auth-gated store for records that can't live on public PDSes — private events, invite-only groups, members-only chat. Set `config.spaces` and contrail exposes the space XRPCs at `{namespace}.space.*` alongside your public indexer: - -```ts -const contrail = new Contrail({ - namespace: "com.example", - collections: { /* ... */ }, - spaces: { - type: "com.example.event.space", // NSID classifying the kind of space - serviceDid: "did:web:example.com", // your deployment's DID - // `resolver` is optional — defaults to a composite did:plc + did:web resolver. - }, -}); -``` - -Each collection you declare also gets a parallel `spaces_records_` table (opt out per-collection via `allowInSpaces: false`). Auth uses atproto service-auth JWTs via `@atcute/xrpc-server`. Access is a simple `read` / `write` permission per member — the space owner is implicit write. Invites are first-class (generated token, hashed-at-rest, expiry + max-uses + revocation). - -**Unified `listRecords`.** The per-collection `listRecords` endpoint accepts three call shapes: - -| Call | Returns | -| --- | --- | -| No auth, no `spaceUri` | Public records only | -| `?spaceUri=…` + service-auth JWT | Records from that one space (ACL-gated) | -| Service-auth JWT, no `spaceUri` | Public records **unioned** with records from every space the caller is a member of | - -The union path runs the public and per-space queries in parallel and merges with a shared keyset cursor, so filters, sorts (`time`, record-field, count), hydration, and references all work across sources. Records from a space carry a `space: ` field in the response. - -Full design, migration story, and known limits: [PERMISSIONED_DATA.md](./PERMISSIONED_DATA.md). - -## Typesafe Client Usage - -You can get fully typed XRPC queries for any Contrail instance using [`@atcute/lex-cli`](https://github.com/mary-ext/atcute). The lexicon files are committed to the repo, so you can pull them directly via the git source. - -### Setup +Changesets drive versioning. `@atmo-dev/contrail` and `@atmo-dev/contrail-sync` are `linked` so their versions stay aligned (they share the realtime wire protocol). ```bash -npm install @atcute/client @atcute/lexicons @atcute/lex-cli -``` - -Create a `lex.config.js` pointing at the Contrail instance's repo: - -```js -import { defineLexiconConfig } from "@atcute/lex-cli"; - -export default defineLexiconConfig({ - outdir: "src/lexicon-types/", - imports: ["@atcute/atproto"], - files: ["lexicons/**/*.json"], - pull: { - outdir: "lexicons/", - sources: [ - { - type: "git", - remote: "https://github.com/USER/REPO.git", // the Contrail instance repo - pattern: ["lexicons-generated/**/*.json", "lexicons-pulled/**/*.json", "lexicons/**/*.json"], - }, - ], - }, -}); -``` - -Then pull and generate: - -```bash -npx lex-cli pull && npx lex-cli generate -``` - -### Usage - -Import the generated types (side-effect import registers them with `@atcute/client`), then query with full type safety: - -```ts -import "./lexicon-types/index.js"; // registers ambient types -import { Client } from "@atcute/client"; - -const rpc = new Client({ handler: simpleFetchHandler({ service: /* your contrail url */ }) }); - -const response = await rpc.get("com.example.rsvp.listRecords", { - params: { status: "going", limit: 10 }, // typed params -}); - -if (response.ok) { - console.log(response.data.records); // typed -} +pnpm changeset # add a changeset +pnpm changeset version # bump versions +pnpm release # build + publish ``` diff --git a/examples/cloudflare-workers/.gitignore b/apps/cloudflare-workers/.gitignore similarity index 100% rename from examples/cloudflare-workers/.gitignore rename to apps/cloudflare-workers/.gitignore diff --git a/examples/cloudflare-workers/README.md b/apps/cloudflare-workers/README.md similarity index 100% rename from examples/cloudflare-workers/README.md rename to apps/cloudflare-workers/README.md diff --git a/examples/cloudflare-workers/config.ts b/apps/cloudflare-workers/config.ts similarity index 100% rename from examples/cloudflare-workers/config.ts rename to apps/cloudflare-workers/config.ts diff --git a/examples/cloudflare-workers/generate.ts b/apps/cloudflare-workers/generate.ts similarity index 82% rename from examples/cloudflare-workers/generate.ts rename to apps/cloudflare-workers/generate.ts index 5e7a91b..61361dd 100644 --- a/examples/cloudflare-workers/generate.ts +++ b/apps/cloudflare-workers/generate.ts @@ -1,7 +1,7 @@ import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { config } from "./config"; -import { generateLexicons } from "@atmo-dev/contrail/generate"; +import { generateLexicons } from "@atmo-dev/contrail-lexicons"; const ROOT_DIR = dirname(fileURLToPath(import.meta.url)); diff --git a/examples/cloudflare-workers/package.json b/apps/cloudflare-workers/package.json similarity index 88% rename from examples/cloudflare-workers/package.json rename to apps/cloudflare-workers/package.json index 91167c7..cffa716 100644 --- a/examples/cloudflare-workers/package.json +++ b/apps/cloudflare-workers/package.json @@ -12,11 +12,12 @@ "sync:remote": "tsx sync.ts --remote" }, "dependencies": { - "@atmo-dev/contrail": "^0.0.3" + "@atmo-dev/contrail": "workspace:*" }, "devDependencies": { "@atcute/lex-cli": "^2.5.3", "@atcute/lexicon-doc": "^2.1.2", + "@atmo-dev/contrail-lexicons": "workspace:*", "@cloudflare/workers-types": "^4.20250124.0", "tsx": "^4.21.0", "typescript": "^5.7.3", diff --git a/examples/cloudflare-workers/sync.ts b/apps/cloudflare-workers/sync.ts similarity index 100% rename from examples/cloudflare-workers/sync.ts rename to apps/cloudflare-workers/sync.ts diff --git a/examples/cloudflare-workers/tsconfig.json b/apps/cloudflare-workers/tsconfig.json similarity index 100% rename from examples/cloudflare-workers/tsconfig.json rename to apps/cloudflare-workers/tsconfig.json diff --git a/examples/cloudflare-workers/worker.ts b/apps/cloudflare-workers/worker.ts similarity index 100% rename from examples/cloudflare-workers/worker.ts rename to apps/cloudflare-workers/worker.ts diff --git a/examples/cloudflare-workers/wrangler.jsonc b/apps/cloudflare-workers/wrangler.jsonc similarity index 100% rename from examples/cloudflare-workers/wrangler.jsonc rename to apps/cloudflare-workers/wrangler.jsonc diff --git a/examples/sveltekit-group-chat/.claude/settings.local.json b/apps/group-chat/.claude/settings.local.json similarity index 100% rename from examples/sveltekit-group-chat/.claude/settings.local.json rename to apps/group-chat/.claude/settings.local.json diff --git a/examples/sveltekit-group-chat/.env.example b/apps/group-chat/.env.example similarity index 100% rename from examples/sveltekit-group-chat/.env.example rename to apps/group-chat/.env.example diff --git a/examples/sveltekit-cloudflare-workers/.gitignore b/apps/group-chat/.gitignore similarity index 100% rename from examples/sveltekit-cloudflare-workers/.gitignore rename to apps/group-chat/.gitignore diff --git a/examples/sveltekit-cloudflare-workers/.npmrc b/apps/group-chat/.npmrc similarity index 100% rename from examples/sveltekit-cloudflare-workers/.npmrc rename to apps/group-chat/.npmrc diff --git a/examples/sveltekit-cloudflare-workers/.prettierignore b/apps/group-chat/.prettierignore similarity index 100% rename from examples/sveltekit-cloudflare-workers/.prettierignore rename to apps/group-chat/.prettierignore diff --git a/examples/sveltekit-cloudflare-workers/.prettierrc b/apps/group-chat/.prettierrc similarity index 100% rename from examples/sveltekit-cloudflare-workers/.prettierrc rename to apps/group-chat/.prettierrc diff --git a/examples/sveltekit-cloudflare-workers/LICENSE b/apps/group-chat/LICENSE similarity index 100% rename from examples/sveltekit-cloudflare-workers/LICENSE rename to apps/group-chat/LICENSE diff --git a/examples/sveltekit-group-chat/README.md b/apps/group-chat/README.md similarity index 100% rename from examples/sveltekit-group-chat/README.md rename to apps/group-chat/README.md diff --git a/examples/sveltekit-cloudflare-workers/eslint.config.js b/apps/group-chat/eslint.config.js similarity index 100% rename from examples/sveltekit-cloudflare-workers/eslint.config.js rename to apps/group-chat/eslint.config.js diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/channel/getRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/channel/listRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/channel/watchRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/adopt.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/defs.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/defs.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/defs.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/delete.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/delete.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/delete.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/deleteRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/getHealth.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/list.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/list.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/list.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/mint.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/mint.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/mint.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/putRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/reauth.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/create.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/delete.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/deleteRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/grant.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/listMembers.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/putRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/resync.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/revoke.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/community/space/setAccessLevel.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getCursor.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/getCursor.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getCursor.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/getCursor.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getOverview.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/getOverview.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getOverview.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/getOverview.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getProfile.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/getProfile.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/getProfile.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/getProfile.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/create.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/create.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/create.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/invite/create.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/invite/defs.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/list.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/list.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/list.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/invite/list.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/invite/redeem.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/invite/revoke.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/message/getRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/message/listRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/message/watchRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/notifyOfUpdate.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/permissionSet.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/subscribe.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/realtime/ticket.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/server/getRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/server/listRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/server/watchRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/addMember.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/createSpace.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/defs.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/defs.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/defs.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/deleteRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/getBlob.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/getRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/getSpace.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/leaveSpace.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/listBlobs.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/listMembers.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/listRecords.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/listSpaces.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/putRecord.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/removeMember.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/space/uploadBlob.json diff --git a/examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json b/apps/group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json rename to apps/group-chat/lexicons-generated/tools/atmo/chat/spaceExt/whoami.json diff --git a/examples/sveltekit-group-chat/lexicons-pulled/README.md b/apps/group-chat/lexicons-pulled/README.md similarity index 100% rename from examples/sveltekit-group-chat/lexicons-pulled/README.md rename to apps/group-chat/lexicons-pulled/README.md diff --git a/examples/sveltekit-cloudflare-workers/lexicons-pulled/app/bsky/actor/profile.json b/apps/group-chat/lexicons-pulled/app/bsky/actor/profile.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-pulled/app/bsky/actor/profile.json rename to apps/group-chat/lexicons-pulled/app/bsky/actor/profile.json diff --git a/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json b/apps/group-chat/lexicons/tools/atmo/chat/channel.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons/tools/atmo/chat/channel.json rename to apps/group-chat/lexicons/tools/atmo/chat/channel.json diff --git a/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json b/apps/group-chat/lexicons/tools/atmo/chat/message.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons/tools/atmo/chat/message.json rename to apps/group-chat/lexicons/tools/atmo/chat/message.json diff --git a/examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json b/apps/group-chat/lexicons/tools/atmo/chat/server.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons/tools/atmo/chat/server.json rename to apps/group-chat/lexicons/tools/atmo/chat/server.json diff --git a/examples/sveltekit-group-chat/package.json b/apps/group-chat/package.json similarity index 96% rename from examples/sveltekit-group-chat/package.json rename to apps/group-chat/package.json index abe6f0c..71d4219 100644 --- a/examples/sveltekit-group-chat/package.json +++ b/apps/group-chat/package.json @@ -60,6 +60,8 @@ "dependencies": { "@atcute/jetstream": "^1.1.2", "@atmo-dev/contrail": "workspace:*", + "@atmo-dev/contrail-sync": "workspace:*", + "@atmo-dev/contrail-lexicons": "workspace:*", "@foxui/core": "^0.9.1", "@foxui/social": "^0.8.10", "@foxui/time": "^0.8.5", diff --git a/examples/sveltekit-cloudflare-workers/pnpm-lock.yaml b/apps/group-chat/pnpm-lock.yaml similarity index 100% rename from examples/sveltekit-cloudflare-workers/pnpm-lock.yaml rename to apps/group-chat/pnpm-lock.yaml diff --git a/examples/sveltekit-group-chat/scripts/append-scheduled.ts b/apps/group-chat/scripts/append-scheduled.ts similarity index 100% rename from examples/sveltekit-group-chat/scripts/append-scheduled.ts rename to apps/group-chat/scripts/append-scheduled.ts diff --git a/examples/sveltekit-group-chat/scripts/generate.ts b/apps/group-chat/scripts/generate.ts similarity index 98% rename from examples/sveltekit-group-chat/scripts/generate.ts rename to apps/group-chat/scripts/generate.ts index 7818daf..205c0f9 100644 --- a/examples/sveltekit-group-chat/scripts/generate.ts +++ b/apps/group-chat/scripts/generate.ts @@ -2,7 +2,7 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { writeFileSync } from 'fs'; import { baseConfig } from '../src/lib/contrail/config'; -import { generateLexicons, extractXrpcMethods } from '@atmo-dev/contrail/generate'; +import { generateLexicons, extractXrpcMethods } from '@atmo-dev/contrail-lexicons'; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/examples/sveltekit-cloudflare-workers/scripts/sync.ts b/apps/group-chat/scripts/sync.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/scripts/sync.ts rename to apps/group-chat/scripts/sync.ts diff --git a/examples/sveltekit-cloudflare-workers/src/app.css b/apps/group-chat/src/app.css similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/app.css rename to apps/group-chat/src/app.css diff --git a/examples/sveltekit-group-chat/src/app.d.ts b/apps/group-chat/src/app.d.ts similarity index 100% rename from examples/sveltekit-group-chat/src/app.d.ts rename to apps/group-chat/src/app.d.ts diff --git a/examples/sveltekit-cloudflare-workers/src/app.html b/apps/group-chat/src/app.html similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/app.html rename to apps/group-chat/src/app.html diff --git a/examples/sveltekit-group-chat/src/hooks.server.ts b/apps/group-chat/src/hooks.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/hooks.server.ts rename to apps/group-chat/src/hooks.server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/auth.svelte.ts b/apps/group-chat/src/lib/atproto/auth.svelte.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/auth.svelte.ts rename to apps/group-chat/src/lib/atproto/auth.svelte.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts b/apps/group-chat/src/lib/atproto/generated-methods.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/generated-methods.ts rename to apps/group-chat/src/lib/atproto/generated-methods.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/image-helper.ts b/apps/group-chat/src/lib/atproto/image-helper.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/image-helper.ts rename to apps/group-chat/src/lib/atproto/image-helper.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/index.ts b/apps/group-chat/src/lib/atproto/index.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/index.ts rename to apps/group-chat/src/lib/atproto/index.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/methods.ts b/apps/group-chat/src/lib/atproto/methods.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/methods.ts rename to apps/group-chat/src/lib/atproto/methods.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/port.ts b/apps/group-chat/src/lib/atproto/port.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/port.ts rename to apps/group-chat/src/lib/atproto/port.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-key.ts b/apps/group-chat/src/lib/atproto/scripts/generate-key.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-key.ts rename to apps/group-chat/src/lib/atproto/scripts/generate-key.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-secret.ts b/apps/group-chat/src/lib/atproto/scripts/generate-secret.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-secret.ts rename to apps/group-chat/src/lib/atproto/scripts/generate-secret.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/setup-dev.ts b/apps/group-chat/src/lib/atproto/scripts/setup-dev.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/setup-dev.ts rename to apps/group-chat/src/lib/atproto/scripts/setup-dev.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/tunnel.ts b/apps/group-chat/src/lib/atproto/scripts/tunnel.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/scripts/tunnel.ts rename to apps/group-chat/src/lib/atproto/scripts/tunnel.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/kv-store.ts b/apps/group-chat/src/lib/atproto/server/kv-store.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/kv-store.ts rename to apps/group-chat/src/lib/atproto/server/kv-store.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.remote.ts b/apps/group-chat/src/lib/atproto/server/oauth.remote.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.remote.ts rename to apps/group-chat/src/lib/atproto/server/oauth.remote.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.ts b/apps/group-chat/src/lib/atproto/server/oauth.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.ts rename to apps/group-chat/src/lib/atproto/server/oauth.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/profile.ts b/apps/group-chat/src/lib/atproto/server/profile.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/profile.ts rename to apps/group-chat/src/lib/atproto/server/profile.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts b/apps/group-chat/src/lib/atproto/server/repo.remote.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/repo.remote.ts rename to apps/group-chat/src/lib/atproto/server/repo.remote.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/session.ts b/apps/group-chat/src/lib/atproto/server/session.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/session.ts rename to apps/group-chat/src/lib/atproto/server/session.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/signed-cookie.ts b/apps/group-chat/src/lib/atproto/server/signed-cookie.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/signed-cookie.ts rename to apps/group-chat/src/lib/atproto/server/signed-cookie.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/settings.ts b/apps/group-chat/src/lib/atproto/settings.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/settings.ts rename to apps/group-chat/src/lib/atproto/settings.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/ui/LoginModal.svelte b/apps/group-chat/src/lib/atproto/ui/LoginModal.svelte similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/ui/LoginModal.svelte rename to apps/group-chat/src/lib/atproto/ui/LoginModal.svelte diff --git a/examples/sveltekit-cloudflare-workers/src/lib/contrail/client.ts b/apps/group-chat/src/lib/contrail/client.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/contrail/client.ts rename to apps/group-chat/src/lib/contrail/client.ts diff --git a/examples/sveltekit-group-chat/src/lib/contrail/config.ts b/apps/group-chat/src/lib/contrail/config.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/contrail/config.ts rename to apps/group-chat/src/lib/contrail/config.ts diff --git a/examples/sveltekit-group-chat/src/lib/contrail/index.ts b/apps/group-chat/src/lib/contrail/index.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/contrail/index.ts rename to apps/group-chat/src/lib/contrail/index.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/emojis/icons.json b/apps/group-chat/src/lib/emojis/icons.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/emojis/icons.json rename to apps/group-chat/src/lib/emojis/icons.json diff --git a/examples/sveltekit-cloudflare-workers/src/lib/emojis/index.ts b/apps/group-chat/src/lib/emojis/index.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/emojis/index.ts rename to apps/group-chat/src/lib/emojis/index.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/index.ts b/apps/group-chat/src/lib/index.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/index.ts rename to apps/group-chat/src/lib/index.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/channels-context.ts b/apps/group-chat/src/lib/rooms/channels-context.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/channels-context.ts rename to apps/group-chat/src/lib/rooms/channels-context.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts b/apps/group-chat/src/lib/rooms/connection.svelte.ts similarity index 94% rename from examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts rename to apps/group-chat/src/lib/rooms/connection.svelte.ts index 831e4e8..95c56d9 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/connection.svelte.ts +++ b/apps/group-chat/src/lib/rooms/connection.svelte.ts @@ -2,7 +2,7 @@ * the current view. The channel page updates this as its WatchQuery status * changes; the server navbar renders a tiny colored dot for it. */ -import type { WatchStoreStatus } from '@atmo-dev/contrail/sync'; +import type { WatchStoreStatus } from '@atmo-dev/contrail-sync'; export const connection = $state<{ status: WatchStoreStatus }>({ status: 'idle' }); diff --git a/examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts b/apps/group-chat/src/lib/rooms/invites.remote.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/invites.remote.ts rename to apps/group-chat/src/lib/rooms/invites.remote.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts b/apps/group-chat/src/lib/rooms/profiles.svelte.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/profiles.svelte.ts rename to apps/group-chat/src/lib/rooms/profiles.svelte.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts b/apps/group-chat/src/lib/rooms/realtime.svelte.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/realtime.svelte.ts rename to apps/group-chat/src/lib/rooms/realtime.svelte.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts b/apps/group-chat/src/lib/rooms/rooms.remote.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/rooms.remote.ts rename to apps/group-chat/src/lib/rooms/rooms.remote.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/server.ts b/apps/group-chat/src/lib/rooms/server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/server.ts rename to apps/group-chat/src/lib/rooms/server.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts b/apps/group-chat/src/lib/rooms/unread.svelte.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/unread.svelte.ts rename to apps/group-chat/src/lib/rooms/unread.svelte.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/uri.ts b/apps/group-chat/src/lib/rooms/uri.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/rooms/uri.ts rename to apps/group-chat/src/lib/rooms/uri.ts diff --git a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts b/apps/group-chat/src/lib/rooms/watch.svelte.ts similarity index 98% rename from examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts rename to apps/group-chat/src/lib/rooms/watch.svelte.ts index 85c2ddb..9271c28 100644 --- a/examples/sveltekit-group-chat/src/lib/rooms/watch.svelte.ts +++ b/apps/group-chat/src/lib/rooms/watch.svelte.ts @@ -1,4 +1,4 @@ -/** Svelte 5 binding over `@atmo-dev/contrail/sync`. +/** Svelte 5 binding over `@atmo-dev/contrail-sync`. * * Usage: * const messagesQuery = $derived( @@ -28,8 +28,8 @@ import { type WatchRecord, type WatchStore, type WatchStoreStatus -} from '@atmo-dev/contrail/sync'; -import { createIndexedDBCache } from '@atmo-dev/contrail/sync/cache-idb'; +} from '@atmo-dev/contrail-sync'; +import { createIndexedDBCache } from '@atmo-dev/contrail-sync/cache-idb'; import type { InferInput } from '@atcute/lexicons'; import type { BaseSchema } from '@atcute/lexicons/validations'; import type { Records, XRPCQueries } from '@atcute/lexicons/ambient'; diff --git a/examples/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth-client-metadata.json/+server.ts b/apps/group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth-client-metadata.json/+server.ts rename to apps/group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/callback/+server.ts b/apps/group-chat/src/routes/(oauth)/oauth/callback/+server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/callback/+server.ts rename to apps/group-chat/src/routes/(oauth)/oauth/callback/+server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/jwks.json/+server.ts b/apps/group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/jwks.json/+server.ts rename to apps/group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/+layout.server.ts b/apps/group-chat/src/routes/+layout.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/+layout.server.ts rename to apps/group-chat/src/routes/+layout.server.ts diff --git a/examples/sveltekit-group-chat/src/routes/+layout.svelte b/apps/group-chat/src/routes/+layout.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/+layout.svelte rename to apps/group-chat/src/routes/+layout.svelte diff --git a/examples/sveltekit-cloudflare-workers/src/routes/+layout.ts b/apps/group-chat/src/routes/+layout.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/+layout.ts rename to apps/group-chat/src/routes/+layout.ts diff --git a/examples/sveltekit-group-chat/src/routes/+page.server.ts b/apps/group-chat/src/routes/+page.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/+page.server.ts rename to apps/group-chat/src/routes/+page.server.ts diff --git a/examples/sveltekit-group-chat/src/routes/+page.svelte b/apps/group-chat/src/routes/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/+page.svelte rename to apps/group-chat/src/routes/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts b/apps/group-chat/src/routes/api/blob-upload/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/api/blob-upload/+server.ts rename to apps/group-chat/src/routes/api/blob-upload/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/api/blob/+server.ts b/apps/group-chat/src/routes/api/blob/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/api/blob/+server.ts rename to apps/group-chat/src/routes/api/blob/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/api/cron/+server.ts b/apps/group-chat/src/routes/api/cron/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/api/cron/+server.ts rename to apps/group-chat/src/routes/api/cron/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts b/apps/group-chat/src/routes/api/ticket/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/api/ticket/+server.ts rename to apps/group-chat/src/routes/api/ticket/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts b/apps/group-chat/src/routes/c/[communityDid]/+layout.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.server.ts rename to apps/group-chat/src/routes/c/[communityDid]/+layout.server.ts diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte b/apps/group-chat/src/routes/c/[communityDid]/+layout.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/+layout.svelte rename to apps/group-chat/src/routes/c/[communityDid]/+layout.svelte diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte b/apps/group-chat/src/routes/c/[communityDid]/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/+page.svelte rename to apps/group-chat/src/routes/c/[communityDid]/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts b/apps/group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts rename to apps/group-chat/src/routes/c/[communityDid]/[channelKey]/+page.server.ts diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte b/apps/group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte rename to apps/group-chat/src/routes/c/[communityDid]/[channelKey]/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts b/apps/group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts rename to apps/group-chat/src/routes/c/[communityDid]/settings/invites/+page.server.ts diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte b/apps/group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte rename to apps/group-chat/src/routes/c/[communityDid]/settings/invites/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts b/apps/group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts rename to apps/group-chat/src/routes/c/[communityDid]/settings/members/+page.server.ts diff --git a/examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte b/apps/group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte rename to apps/group-chat/src/routes/c/[communityDid]/settings/members/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte b/apps/group-chat/src/routes/join/[token]/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/join/[token]/+page.svelte rename to apps/group-chat/src/routes/join/[token]/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/new/+page.svelte b/apps/group-chat/src/routes/new/+page.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/routes/new/+page.svelte rename to apps/group-chat/src/routes/new/+page.svelte diff --git a/examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts b/apps/group-chat/src/routes/xrpc/[...path]/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/xrpc/[...path]/+server.ts rename to apps/group-chat/src/routes/xrpc/[...path]/+server.ts diff --git a/examples/sveltekit-cloudflare-workers/static/favicon.png b/apps/group-chat/static/favicon.png similarity index 100% rename from examples/sveltekit-cloudflare-workers/static/favicon.png rename to apps/group-chat/static/favicon.png diff --git a/examples/sveltekit-cloudflare-workers/svelte.config.js b/apps/group-chat/svelte.config.js similarity index 100% rename from examples/sveltekit-cloudflare-workers/svelte.config.js rename to apps/group-chat/svelte.config.js diff --git a/examples/sveltekit-cloudflare-workers/tsconfig.json b/apps/group-chat/tsconfig.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/tsconfig.json rename to apps/group-chat/tsconfig.json diff --git a/examples/sveltekit-cloudflare-workers/vite.config.ts b/apps/group-chat/vite.config.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/vite.config.ts rename to apps/group-chat/vite.config.ts diff --git a/examples/sveltekit-group-chat/wrangler.jsonc b/apps/group-chat/wrangler.jsonc similarity index 100% rename from examples/sveltekit-group-chat/wrangler.jsonc rename to apps/group-chat/wrangler.jsonc diff --git a/examples/postgres/README.md b/apps/postgres/README.md similarity index 100% rename from examples/postgres/README.md rename to apps/postgres/README.md diff --git a/examples/postgres/config.ts b/apps/postgres/config.ts similarity index 100% rename from examples/postgres/config.ts rename to apps/postgres/config.ts diff --git a/examples/postgres/docker-compose.yml b/apps/postgres/docker-compose.yml similarity index 100% rename from examples/postgres/docker-compose.yml rename to apps/postgres/docker-compose.yml diff --git a/examples/postgres/ingest.ts b/apps/postgres/ingest.ts similarity index 100% rename from examples/postgres/ingest.ts rename to apps/postgres/ingest.ts diff --git a/examples/postgres/package.json b/apps/postgres/package.json similarity index 89% rename from examples/postgres/package.json rename to apps/postgres/package.json index 2ff5ca8..6b6b853 100644 --- a/examples/postgres/package.json +++ b/apps/postgres/package.json @@ -9,7 +9,7 @@ "serve": "tsx serve.ts" }, "dependencies": { - "@atmo-dev/contrail": "^0.0.5", + "@atmo-dev/contrail": "workspace:*", "pg": "^8.20.0" }, "devDependencies": { diff --git a/examples/postgres/serve.ts b/apps/postgres/serve.ts similarity index 100% rename from examples/postgres/serve.ts rename to apps/postgres/serve.ts diff --git a/examples/postgres/sync.ts b/apps/postgres/sync.ts similarity index 100% rename from examples/postgres/sync.ts rename to apps/postgres/sync.ts diff --git a/examples/postgres/tsconfig.json b/apps/postgres/tsconfig.json similarity index 100% rename from examples/postgres/tsconfig.json rename to apps/postgres/tsconfig.json diff --git a/Dockerfile b/apps/rsvp-atmo/Dockerfile similarity index 100% rename from Dockerfile rename to apps/rsvp-atmo/Dockerfile diff --git a/lexicons-generated/rsvp/atmo/community/adopt.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/adopt.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/adopt.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/adopt.json diff --git a/lexicons-generated/rsvp/atmo/community/defs.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/defs.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/defs.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/defs.json diff --git a/lexicons-generated/rsvp/atmo/community/delete.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/delete.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/delete.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/delete.json diff --git a/lexicons-generated/rsvp/atmo/community/deleteRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/deleteRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/deleteRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/deleteRecord.json diff --git a/lexicons-generated/rsvp/atmo/community/getHealth.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/getHealth.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/getHealth.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/getHealth.json diff --git a/lexicons-generated/rsvp/atmo/community/invite/create.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/create.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/invite/create.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/create.json diff --git a/lexicons-generated/rsvp/atmo/community/invite/list.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/list.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/invite/list.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/list.json diff --git a/lexicons-generated/rsvp/atmo/community/invite/redeem.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/redeem.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/invite/redeem.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/redeem.json diff --git a/lexicons-generated/rsvp/atmo/community/invite/revoke.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/revoke.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/invite/revoke.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/invite/revoke.json diff --git a/lexicons-generated/rsvp/atmo/community/list.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/list.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/list.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/list.json diff --git a/lexicons-generated/rsvp/atmo/community/mint.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/mint.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/mint.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/mint.json diff --git a/lexicons-generated/rsvp/atmo/community/putRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/putRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/putRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/putRecord.json diff --git a/lexicons-generated/rsvp/atmo/community/reauth.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/reauth.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/reauth.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/reauth.json diff --git a/lexicons-generated/rsvp/atmo/community/space/create.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/create.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/create.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/create.json diff --git a/lexicons-generated/rsvp/atmo/community/space/delete.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/delete.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/delete.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/delete.json diff --git a/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/deleteRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/deleteRecord.json diff --git a/lexicons-generated/rsvp/atmo/community/space/grant.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/grant.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/grant.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/grant.json diff --git a/lexicons-generated/rsvp/atmo/community/space/listMembers.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/listMembers.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/listMembers.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/listMembers.json diff --git a/lexicons-generated/rsvp/atmo/community/space/putRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/putRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/putRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/putRecord.json diff --git a/lexicons-generated/rsvp/atmo/community/space/resync.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/resync.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/resync.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/resync.json diff --git a/lexicons-generated/rsvp/atmo/community/space/revoke.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/revoke.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/revoke.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/revoke.json diff --git a/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/setAccessLevel.json diff --git a/lexicons-generated/rsvp/atmo/community/space/whoami.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/whoami.json similarity index 100% rename from lexicons-generated/rsvp/atmo/community/space/whoami.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/community/space/whoami.json diff --git a/lexicons-generated/rsvp/atmo/event/getRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/getRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/event/getRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/getRecord.json diff --git a/lexicons-generated/rsvp/atmo/event/listRecords.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/listRecords.json similarity index 100% rename from lexicons-generated/rsvp/atmo/event/listRecords.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/event/listRecords.json diff --git a/lexicons-generated/rsvp/atmo/getCursor.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getCursor.json similarity index 100% rename from lexicons-generated/rsvp/atmo/getCursor.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getCursor.json diff --git a/lexicons-generated/rsvp/atmo/getOverview.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getOverview.json similarity index 100% rename from lexicons-generated/rsvp/atmo/getOverview.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getOverview.json diff --git a/lexicons-generated/rsvp/atmo/getProfile.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getProfile.json similarity index 100% rename from lexicons-generated/rsvp/atmo/getProfile.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/getProfile.json diff --git a/lexicons-generated/rsvp/atmo/notifyOfUpdate.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/notifyOfUpdate.json similarity index 100% rename from lexicons-generated/rsvp/atmo/notifyOfUpdate.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/notifyOfUpdate.json diff --git a/lexicons-generated/rsvp/atmo/permissionSet.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/permissionSet.json similarity index 100% rename from lexicons-generated/rsvp/atmo/permissionSet.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/permissionSet.json diff --git a/lexicons-generated/rsvp/atmo/rsvp/getRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/getRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/rsvp/getRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/getRecord.json diff --git a/lexicons-generated/rsvp/atmo/rsvp/listRecords.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/listRecords.json similarity index 100% rename from lexicons-generated/rsvp/atmo/rsvp/listRecords.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/rsvp/listRecords.json diff --git a/lexicons-generated/rsvp/atmo/space/addMember.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/addMember.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/addMember.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/addMember.json diff --git a/lexicons-generated/rsvp/atmo/space/createSpace.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/createSpace.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/createSpace.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/createSpace.json diff --git a/lexicons-generated/rsvp/atmo/space/defs.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/defs.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/defs.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/defs.json diff --git a/lexicons-generated/rsvp/atmo/space/deleteRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/deleteRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/deleteRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/deleteRecord.json diff --git a/lexicons-generated/rsvp/atmo/space/getBlob.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getBlob.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/getBlob.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getBlob.json diff --git a/lexicons-generated/rsvp/atmo/space/getRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/getRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getRecord.json diff --git a/lexicons-generated/rsvp/atmo/space/getSpace.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getSpace.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/getSpace.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/getSpace.json diff --git a/lexicons-generated/rsvp/atmo/space/leaveSpace.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/leaveSpace.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/leaveSpace.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/leaveSpace.json diff --git a/lexicons-generated/rsvp/atmo/space/listBlobs.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listBlobs.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/listBlobs.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listBlobs.json diff --git a/lexicons-generated/rsvp/atmo/space/listMembers.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listMembers.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/listMembers.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listMembers.json diff --git a/lexicons-generated/rsvp/atmo/space/listRecords.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listRecords.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/listRecords.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listRecords.json diff --git a/lexicons-generated/rsvp/atmo/space/listSpaces.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listSpaces.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/listSpaces.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/listSpaces.json diff --git a/lexicons-generated/rsvp/atmo/space/putRecord.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/putRecord.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/putRecord.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/putRecord.json diff --git a/lexicons-generated/rsvp/atmo/space/removeMember.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/removeMember.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/removeMember.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/removeMember.json diff --git a/lexicons-generated/rsvp/atmo/space/uploadBlob.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/uploadBlob.json similarity index 100% rename from lexicons-generated/rsvp/atmo/space/uploadBlob.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/space/uploadBlob.json diff --git a/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json similarity index 100% rename from lexicons-generated/rsvp/atmo/spaceExt/invite/create.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/create.json diff --git a/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json similarity index 100% rename from lexicons-generated/rsvp/atmo/spaceExt/invite/list.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/list.json diff --git a/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json similarity index 100% rename from lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/redeem.json diff --git a/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json similarity index 100% rename from lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/invite/revoke.json diff --git a/lexicons-generated/rsvp/atmo/spaceExt/whoami.json b/apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/whoami.json similarity index 100% rename from lexicons-generated/rsvp/atmo/spaceExt/whoami.json rename to apps/rsvp-atmo/lexicons-generated/rsvp/atmo/spaceExt/whoami.json diff --git a/lexicons-pulled/README.md b/apps/rsvp-atmo/lexicons-pulled/README.md similarity index 100% rename from lexicons-pulled/README.md rename to apps/rsvp-atmo/lexicons-pulled/README.md diff --git a/examples/sveltekit-group-chat/lexicons-pulled/app/bsky/actor/profile.json b/apps/rsvp-atmo/lexicons-pulled/app/bsky/actor/profile.json similarity index 100% rename from examples/sveltekit-group-chat/lexicons-pulled/app/bsky/actor/profile.json rename to apps/rsvp-atmo/lexicons-pulled/app/bsky/actor/profile.json diff --git a/lexicons-pulled/community/lexicon/calendar/event.json b/apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/event.json similarity index 100% rename from lexicons-pulled/community/lexicon/calendar/event.json rename to apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/event.json diff --git a/lexicons-pulled/community/lexicon/calendar/rsvp.json b/apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/rsvp.json similarity index 100% rename from lexicons-pulled/community/lexicon/calendar/rsvp.json rename to apps/rsvp-atmo/lexicons-pulled/community/lexicon/calendar/rsvp.json diff --git a/lexicons-pulled/community/lexicon/location/address.json b/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/address.json similarity index 100% rename from lexicons-pulled/community/lexicon/location/address.json rename to apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/address.json diff --git a/lexicons-pulled/community/lexicon/location/fsq.json b/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/fsq.json similarity index 100% rename from lexicons-pulled/community/lexicon/location/fsq.json rename to apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/fsq.json diff --git a/lexicons-pulled/community/lexicon/location/geo.json b/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/geo.json similarity index 100% rename from lexicons-pulled/community/lexicon/location/geo.json rename to apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/geo.json diff --git a/lexicons-pulled/community/lexicon/location/hthree.json b/apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/hthree.json similarity index 100% rename from lexicons-pulled/community/lexicon/location/hthree.json rename to apps/rsvp-atmo/lexicons-pulled/community/lexicon/location/hthree.json diff --git a/lexicons/README.md b/apps/rsvp-atmo/lexicons/README.md similarity index 100% rename from lexicons/README.md rename to apps/rsvp-atmo/lexicons/README.md diff --git a/apps/rsvp-atmo/package.json b/apps/rsvp-atmo/package.json new file mode 100644 index 0000000..8503756 --- /dev/null +++ b/apps/rsvp-atmo/package.json @@ -0,0 +1,35 @@ +{ + "name": "rsvp-atmo", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "Reference Cloudflare Workers deployment of contrail — indexes community.lexicon.calendar.event/rsvp on D1 with the full spaces/community/realtime surface enabled.", + "scripts": { + "dev": "wrangler dev --test-scheduled", + "dev:auto": "tsx scripts/dev.ts", + "deploy": "wrangler deploy", + "clean": "tsx scripts/clean.ts", + "generate": "tsx src/generate.ts", + "generate:pull": "contrail-lex all --config src/config.ts", + "publish-lexicons": "tsx scripts/publish-lexicons.ts", + "typecheck": "tsc --noEmit", + "ingest": "curl -s http://localhost:8787/__scheduled?cron=*/1+*+*+*+*", + "sync": "tsx src/sync.ts" + }, + "dependencies": { + "@atmo-dev/contrail": "workspace:*" + }, + "devDependencies": { + "@atcute/atproto": "^3.1.10", + "@atcute/client": "^4.2.1", + "@atcute/lex-cli": "^2.5.3", + "@atcute/lexicon-doc": "^2.1.2", + "@atcute/lexicons": "^1.2.9", + "@atmo-dev/contrail-lexicons": "workspace:*", + "@cloudflare/workers-types": "^4.20250124.0", + "@types/node": "^25.5.0", + "tsx": "^4.21.0", + "typescript": "^5.7.3", + "wrangler": "^4.63.0" + } +} diff --git a/scripts/clean.ts b/apps/rsvp-atmo/scripts/clean.ts similarity index 100% rename from scripts/clean.ts rename to apps/rsvp-atmo/scripts/clean.ts diff --git a/scripts/dev.ts b/apps/rsvp-atmo/scripts/dev.ts similarity index 100% rename from scripts/dev.ts rename to apps/rsvp-atmo/scripts/dev.ts diff --git a/scripts/publish-lexicons.ts b/apps/rsvp-atmo/scripts/publish-lexicons.ts similarity index 94% rename from scripts/publish-lexicons.ts rename to apps/rsvp-atmo/scripts/publish-lexicons.ts index d497f2d..f5909f9 100644 --- a/scripts/publish-lexicons.ts +++ b/apps/rsvp-atmo/scripts/publish-lexicons.ts @@ -11,7 +11,7 @@ */ import { join } from "node:path"; -import { publishLexicons } from "../src/publish"; +import { publishLexicons } from "@atmo-dev/contrail-lexicons"; const ROOT = join(import.meta.dirname, ".."); diff --git a/app/config.ts b/apps/rsvp-atmo/src/config.ts similarity index 96% rename from app/config.ts rename to apps/rsvp-atmo/src/config.ts index 6dd74dc..e5fa2e3 100644 --- a/app/config.ts +++ b/apps/rsvp-atmo/src/config.ts @@ -1,4 +1,4 @@ -import type { ContrailConfig } from "../src/index"; +import type { ContrailConfig } from "@atmo-dev/contrail"; export const config: ContrailConfig = { namespace: "rsvp.atmo", diff --git a/app/generate.ts b/apps/rsvp-atmo/src/generate.ts similarity index 86% rename from app/generate.ts rename to apps/rsvp-atmo/src/generate.ts index c359b24..2239922 100644 --- a/app/generate.ts +++ b/apps/rsvp-atmo/src/generate.ts @@ -6,7 +6,7 @@ import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { config } from "./config"; -import { generateLexicons } from "../src/generate"; +import { generateLexicons } from "@atmo-dev/contrail-lexicons"; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); diff --git a/app/sync.ts b/apps/rsvp-atmo/src/sync.ts similarity index 97% rename from app/sync.ts rename to apps/rsvp-atmo/src/sync.ts index d6f6580..bf25bc9 100644 --- a/app/sync.ts +++ b/apps/rsvp-atmo/src/sync.ts @@ -5,7 +5,7 @@ * pnpm sync # local D1 * pnpm sync --remote # prod D1 */ -import { Contrail } from "../src/index"; +import { Contrail } from "@atmo-dev/contrail"; import { config } from "./config"; import { getPlatformProxy } from "wrangler"; diff --git a/app/worker.ts b/apps/rsvp-atmo/src/worker.ts similarity index 85% rename from app/worker.ts rename to apps/rsvp-atmo/src/worker.ts index 00a60a8..b046d44 100644 --- a/app/worker.ts +++ b/apps/rsvp-atmo/src/worker.ts @@ -1,5 +1,5 @@ -import { Contrail } from "../src/index"; -import { createHandler } from "../src/server"; +import { Contrail } from "@atmo-dev/contrail"; +import { createHandler } from "@atmo-dev/contrail/server"; import { config } from "./config"; const contrail = new Contrail(config); diff --git a/apps/rsvp-atmo/tsconfig.json b/apps/rsvp-atmo/tsconfig.json new file mode 100644 index 0000000..bb57862 --- /dev/null +++ b/apps/rsvp-atmo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types", "node"] + }, + "include": ["src", "scripts"] +} diff --git a/wrangler.jsonc b/apps/rsvp-atmo/wrangler.jsonc similarity index 93% rename from wrangler.jsonc rename to apps/rsvp-atmo/wrangler.jsonc index 2eb97e0..9f223f2 100644 --- a/wrangler.jsonc +++ b/apps/rsvp-atmo/wrangler.jsonc @@ -1,7 +1,7 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "contrail", - "main": "app/worker.ts", + "main": "src/worker.ts", "compatibility_date": "2025-12-25", "observability": { "enabled": true diff --git a/examples/sveltekit-cloudflare-workers/.claude/settings.local.json b/apps/sveltekit-cloudflare-workers/.claude/settings.local.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/.claude/settings.local.json rename to apps/sveltekit-cloudflare-workers/.claude/settings.local.json diff --git a/examples/sveltekit-cloudflare-workers/.env.example b/apps/sveltekit-cloudflare-workers/.env.example similarity index 100% rename from examples/sveltekit-cloudflare-workers/.env.example rename to apps/sveltekit-cloudflare-workers/.env.example diff --git a/examples/sveltekit-group-chat/.gitignore b/apps/sveltekit-cloudflare-workers/.gitignore similarity index 100% rename from examples/sveltekit-group-chat/.gitignore rename to apps/sveltekit-cloudflare-workers/.gitignore diff --git a/examples/sveltekit-group-chat/.npmrc b/apps/sveltekit-cloudflare-workers/.npmrc similarity index 100% rename from examples/sveltekit-group-chat/.npmrc rename to apps/sveltekit-cloudflare-workers/.npmrc diff --git a/examples/sveltekit-group-chat/.prettierignore b/apps/sveltekit-cloudflare-workers/.prettierignore similarity index 100% rename from examples/sveltekit-group-chat/.prettierignore rename to apps/sveltekit-cloudflare-workers/.prettierignore diff --git a/examples/sveltekit-group-chat/.prettierrc b/apps/sveltekit-cloudflare-workers/.prettierrc similarity index 100% rename from examples/sveltekit-group-chat/.prettierrc rename to apps/sveltekit-cloudflare-workers/.prettierrc diff --git a/examples/sveltekit-group-chat/LICENSE b/apps/sveltekit-cloudflare-workers/LICENSE similarity index 100% rename from examples/sveltekit-group-chat/LICENSE rename to apps/sveltekit-cloudflare-workers/LICENSE diff --git a/examples/sveltekit-cloudflare-workers/README.md b/apps/sveltekit-cloudflare-workers/README.md similarity index 100% rename from examples/sveltekit-cloudflare-workers/README.md rename to apps/sveltekit-cloudflare-workers/README.md diff --git a/examples/sveltekit-group-chat/eslint.config.js b/apps/sveltekit-cloudflare-workers/eslint.config.js similarity index 100% rename from examples/sveltekit-group-chat/eslint.config.js rename to apps/sveltekit-cloudflare-workers/eslint.config.js diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getCursor.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getOverview.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/notifyOfUpdate.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json b/apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json rename to apps/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-pulled/README.md b/apps/sveltekit-cloudflare-workers/lexicons-pulled/README.md similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-pulled/README.md rename to apps/sveltekit-cloudflare-workers/lexicons-pulled/README.md diff --git a/lexicons-pulled/app/bsky/actor/profile.json b/apps/sveltekit-cloudflare-workers/lexicons-pulled/app/bsky/actor/profile.json similarity index 100% rename from lexicons-pulled/app/bsky/actor/profile.json rename to apps/sveltekit-cloudflare-workers/lexicons-pulled/app/bsky/actor/profile.json diff --git a/examples/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json b/apps/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json similarity index 100% rename from examples/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json rename to apps/sveltekit-cloudflare-workers/lexicons-pulled/xyz/statusphere/status.json diff --git a/examples/sveltekit-cloudflare-workers/package.json b/apps/sveltekit-cloudflare-workers/package.json similarity index 96% rename from examples/sveltekit-cloudflare-workers/package.json rename to apps/sveltekit-cloudflare-workers/package.json index f45b5ab..9c97ee5 100644 --- a/examples/sveltekit-cloudflare-workers/package.json +++ b/apps/sveltekit-cloudflare-workers/package.json @@ -59,7 +59,8 @@ "license": "MIT", "dependencies": { "@atcute/jetstream": "^1.1.2", - "@atmo-dev/contrail": "^0.1.1", + "@atmo-dev/contrail": "workspace:*", + "@atmo-dev/contrail-lexicons": "workspace:*", "@foxui/core": "^0.9.1", "@foxui/social": "^0.8.10", "@foxui/time": "^0.8.5", diff --git a/examples/sveltekit-cloudflare-workers/scripts/append-scheduled.ts b/apps/sveltekit-cloudflare-workers/scripts/append-scheduled.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/scripts/append-scheduled.ts rename to apps/sveltekit-cloudflare-workers/scripts/append-scheduled.ts diff --git a/examples/sveltekit-cloudflare-workers/scripts/generate.ts b/apps/sveltekit-cloudflare-workers/scripts/generate.ts similarity index 83% rename from examples/sveltekit-cloudflare-workers/scripts/generate.ts rename to apps/sveltekit-cloudflare-workers/scripts/generate.ts index 5a1cb3a..6dd6fde 100644 --- a/examples/sveltekit-cloudflare-workers/scripts/generate.ts +++ b/apps/sveltekit-cloudflare-workers/scripts/generate.ts @@ -1,7 +1,7 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { config } from '../src/lib/contrail/config'; -import { generateLexicons } from '@atmo-dev/contrail/generate'; +import { generateLexicons } from '@atmo-dev/contrail-lexicons'; const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/examples/sveltekit-group-chat/scripts/sync.ts b/apps/sveltekit-cloudflare-workers/scripts/sync.ts similarity index 100% rename from examples/sveltekit-group-chat/scripts/sync.ts rename to apps/sveltekit-cloudflare-workers/scripts/sync.ts diff --git a/examples/sveltekit-group-chat/src/app.css b/apps/sveltekit-cloudflare-workers/src/app.css similarity index 100% rename from examples/sveltekit-group-chat/src/app.css rename to apps/sveltekit-cloudflare-workers/src/app.css diff --git a/examples/sveltekit-cloudflare-workers/src/app.d.ts b/apps/sveltekit-cloudflare-workers/src/app.d.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/app.d.ts rename to apps/sveltekit-cloudflare-workers/src/app.d.ts diff --git a/examples/sveltekit-group-chat/src/app.html b/apps/sveltekit-cloudflare-workers/src/app.html similarity index 100% rename from examples/sveltekit-group-chat/src/app.html rename to apps/sveltekit-cloudflare-workers/src/app.html diff --git a/examples/sveltekit-cloudflare-workers/src/hooks.server.ts b/apps/sveltekit-cloudflare-workers/src/hooks.server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/hooks.server.ts rename to apps/sveltekit-cloudflare-workers/src/hooks.server.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/auth.svelte.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/auth.svelte.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/auth.svelte.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/auth.svelte.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/image-helper.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/image-helper.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/image-helper.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/image-helper.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/index.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/index.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/index.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/index.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/methods.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/methods.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/methods.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/methods.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/port.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/port.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/port.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/port.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-key.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-key.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-key.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-key.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-secret.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-secret.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/scripts/generate-secret.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/generate-secret.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/setup-dev.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/setup-dev.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/scripts/setup-dev.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/setup-dev.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/scripts/tunnel.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/tunnel.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/scripts/tunnel.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/scripts/tunnel.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/kv-store.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/kv-store.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/kv-store.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/kv-store.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.remote.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.remote.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/oauth.remote.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.remote.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/oauth.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/oauth.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/oauth.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/profile.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/profile.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/profile.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/profile.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/server/repo.remote.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/repo.remote.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/server/repo.remote.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/repo.remote.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/session.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/session.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/session.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/session.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/server/signed-cookie.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/server/signed-cookie.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/server/signed-cookie.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/atproto/settings.ts b/apps/sveltekit-cloudflare-workers/src/lib/atproto/settings.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/atproto/settings.ts rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/settings.ts diff --git a/examples/sveltekit-group-chat/src/lib/atproto/ui/LoginModal.svelte b/apps/sveltekit-cloudflare-workers/src/lib/atproto/ui/LoginModal.svelte similarity index 100% rename from examples/sveltekit-group-chat/src/lib/atproto/ui/LoginModal.svelte rename to apps/sveltekit-cloudflare-workers/src/lib/atproto/ui/LoginModal.svelte diff --git a/examples/sveltekit-group-chat/src/lib/contrail/client.ts b/apps/sveltekit-cloudflare-workers/src/lib/contrail/client.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/contrail/client.ts rename to apps/sveltekit-cloudflare-workers/src/lib/contrail/client.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/contrail/config.ts b/apps/sveltekit-cloudflare-workers/src/lib/contrail/config.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/contrail/config.ts rename to apps/sveltekit-cloudflare-workers/src/lib/contrail/config.ts diff --git a/examples/sveltekit-cloudflare-workers/src/lib/contrail/index.ts b/apps/sveltekit-cloudflare-workers/src/lib/contrail/index.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/lib/contrail/index.ts rename to apps/sveltekit-cloudflare-workers/src/lib/contrail/index.ts diff --git a/examples/sveltekit-group-chat/src/lib/emojis/icons.json b/apps/sveltekit-cloudflare-workers/src/lib/emojis/icons.json similarity index 100% rename from examples/sveltekit-group-chat/src/lib/emojis/icons.json rename to apps/sveltekit-cloudflare-workers/src/lib/emojis/icons.json diff --git a/examples/sveltekit-group-chat/src/lib/emojis/index.ts b/apps/sveltekit-cloudflare-workers/src/lib/emojis/index.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/emojis/index.ts rename to apps/sveltekit-cloudflare-workers/src/lib/emojis/index.ts diff --git a/examples/sveltekit-group-chat/src/lib/index.ts b/apps/sveltekit-cloudflare-workers/src/lib/index.ts similarity index 100% rename from examples/sveltekit-group-chat/src/lib/index.ts rename to apps/sveltekit-cloudflare-workers/src/lib/index.ts diff --git a/examples/sveltekit-group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts b/apps/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth-client-metadata.json/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/(oauth)/oauth-client-metadata.json/+server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth-client-metadata.json/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/callback/+server.ts b/apps/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/callback/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/(oauth)/oauth/callback/+server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/callback/+server.ts diff --git a/examples/sveltekit-group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts b/apps/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/jwks.json/+server.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/(oauth)/oauth/jwks.json/+server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/(oauth)/oauth/jwks.json/+server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/+layout.server.ts b/apps/sveltekit-cloudflare-workers/src/routes/+layout.server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/+layout.server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/+layout.server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/+layout.svelte b/apps/sveltekit-cloudflare-workers/src/routes/+layout.svelte similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/+layout.svelte rename to apps/sveltekit-cloudflare-workers/src/routes/+layout.svelte diff --git a/examples/sveltekit-group-chat/src/routes/+layout.ts b/apps/sveltekit-cloudflare-workers/src/routes/+layout.ts similarity index 100% rename from examples/sveltekit-group-chat/src/routes/+layout.ts rename to apps/sveltekit-cloudflare-workers/src/routes/+layout.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/+page.server.ts b/apps/sveltekit-cloudflare-workers/src/routes/+page.server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/+page.server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/+page.server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/+page.svelte b/apps/sveltekit-cloudflare-workers/src/routes/+page.svelte similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/+page.svelte rename to apps/sveltekit-cloudflare-workers/src/routes/+page.svelte diff --git a/examples/sveltekit-cloudflare-workers/src/routes/api/cron/+server.ts b/apps/sveltekit-cloudflare-workers/src/routes/api/cron/+server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/api/cron/+server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/api/cron/+server.ts diff --git a/examples/sveltekit-cloudflare-workers/src/routes/xrpc/[...path]/+server.ts b/apps/sveltekit-cloudflare-workers/src/routes/xrpc/[...path]/+server.ts similarity index 100% rename from examples/sveltekit-cloudflare-workers/src/routes/xrpc/[...path]/+server.ts rename to apps/sveltekit-cloudflare-workers/src/routes/xrpc/[...path]/+server.ts diff --git a/examples/sveltekit-group-chat/static/favicon.png b/apps/sveltekit-cloudflare-workers/static/favicon.png similarity index 100% rename from examples/sveltekit-group-chat/static/favicon.png rename to apps/sveltekit-cloudflare-workers/static/favicon.png diff --git a/examples/sveltekit-cloudflare-workers/static/og.png b/apps/sveltekit-cloudflare-workers/static/og.png similarity index 100% rename from examples/sveltekit-cloudflare-workers/static/og.png rename to apps/sveltekit-cloudflare-workers/static/og.png diff --git a/examples/sveltekit-group-chat/svelte.config.js b/apps/sveltekit-cloudflare-workers/svelte.config.js similarity index 100% rename from examples/sveltekit-group-chat/svelte.config.js rename to apps/sveltekit-cloudflare-workers/svelte.config.js diff --git a/examples/sveltekit-group-chat/tsconfig.json b/apps/sveltekit-cloudflare-workers/tsconfig.json similarity index 100% rename from examples/sveltekit-group-chat/tsconfig.json rename to apps/sveltekit-cloudflare-workers/tsconfig.json diff --git a/examples/sveltekit-group-chat/vite.config.ts b/apps/sveltekit-cloudflare-workers/vite.config.ts similarity index 100% rename from examples/sveltekit-group-chat/vite.config.ts rename to apps/sveltekit-cloudflare-workers/vite.config.ts diff --git a/examples/sveltekit-cloudflare-workers/wrangler.jsonc b/apps/sveltekit-cloudflare-workers/wrangler.jsonc similarity index 100% rename from examples/sveltekit-cloudflare-workers/wrangler.jsonc rename to apps/sveltekit-cloudflare-workers/wrangler.jsonc diff --git a/bin/ingest.ts b/bin/ingest.ts deleted file mode 100644 index a7d3b69..0000000 --- a/bin/ingest.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Persistent Jetstream ingestion with runtime configuration. - * - * Connects to an externally-provided database and subscribes to Jetstream. - * The container does not own the database — it receives a connection URL. - * - * Environment: - * JETSTREAM_URL - Jetstream WebSocket URL (required) - * CONTRAIL_ADAPTER - "postgres" or "sqlite" (default: "postgres") - * DATABASE_URL - PostgreSQL connection string (required when adapter=postgres) - * SQLITE_PATH - SQLite file path (required when adapter=sqlite) - * CONTRAIL_CONFIG - Path to config file (default: "/app/config/contrail.config.ts") - * FLUSH_INTERVAL_MS - Batch flush interval in ms (default: 500) - */ - -import { Contrail } from "../src/index.ts"; -import type { ContrailConfig, Database } from "../src/index.ts"; - -const ADAPTER = process.env.CONTRAIL_ADAPTER ?? "postgres"; -const JETSTREAM_URL = process.env.JETSTREAM_URL; -const CONFIG_PATH = process.env.CONTRAIL_CONFIG ?? "/app/config/contrail.config.ts"; -const FLUSH_INTERVAL_MS = parseInt(process.env.FLUSH_INTERVAL_MS ?? "500", 10); - -async function createDatabase(): Promise<{ db: Database; cleanup: () => Promise }> { - if (ADAPTER === "postgres") { - const url = process.env.DATABASE_URL; - if (!url) { - console.error("DATABASE_URL is required when CONTRAIL_ADAPTER=postgres"); - process.exit(1); - } - const pg = await import("pg"); - const { createPostgresDatabase } = await import("../src/adapters/postgres.ts"); - const pool = new pg.default.Pool({ connectionString: url }); - return { db: createPostgresDatabase(pool), cleanup: () => pool.end() }; - } - - if (ADAPTER === "sqlite") { - const path = process.env.SQLITE_PATH; - if (!path) { - console.error("SQLITE_PATH is required when CONTRAIL_ADAPTER=sqlite"); - process.exit(1); - } - const { createSqliteDatabase } = await import("../src/adapters/sqlite.ts"); - return { db: createSqliteDatabase(path), cleanup: async () => {} }; - } - - console.error(`Unknown adapter: ${ADAPTER}. Use "postgres" or "sqlite".`); - process.exit(1); -} - -async function loadConfig(): Promise { - const module = await import(CONFIG_PATH); - return module.config ?? module.default; -} - -async function main() { - if (!JETSTREAM_URL) { - console.error("JETSTREAM_URL is required"); - process.exit(1); - } - - const config = await loadConfig(); - config.jetstreams = [JETSTREAM_URL]; - - const { db, cleanup } = await createDatabase(); - const contrail = new Contrail({ ...config, db }); - - const controller = new AbortController(); - const shutdown = () => { - console.log("\nShutting down gracefully..."); - controller.abort(); - }; - process.on("SIGTERM", shutdown); - process.on("SIGINT", shutdown); - - console.log("Contrail ingester starting"); - console.log(` Adapter: ${ADAPTER}`); - console.log(` Jetstream: ${JETSTREAM_URL}`); - console.log(` Config: ${CONFIG_PATH}`); - console.log(` Flush: ${FLUSH_INTERVAL_MS}ms`); - - await contrail.runPersistent({ - signal: controller.signal, - flushIntervalMs: FLUSH_INTERVAL_MS, - }); - - await cleanup(); - console.log("Done."); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/examples/sveltekit-group-chat/pnpm-lock.yaml b/examples/sveltekit-group-chat/pnpm-lock.yaml deleted file mode 100644 index 35f6a60..0000000 --- a/examples/sveltekit-group-chat/pnpm-lock.yaml +++ /dev/null @@ -1,4750 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@atcute/jetstream': - specifier: ^1.1.2 - version: 1.1.2 - '@foxui/core': - specifier: ^0.8.2 - version: 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) - '@foxui/social': - specifier: ^0.8.4 - version: 0.8.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2) - '@foxui/time': - specifier: ^0.8.2 - version: 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) - contrail: - specifier: github:flo-bit/contrail - version: https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf(@atcute/identity@1.1.4) - valibot: - specifier: ^1.3.1 - version: 1.3.1(typescript@6.0.2) - devDependencies: - '@atcute/atproto': - specifier: ^3.1.10 - version: 3.1.10 - '@atcute/bluesky': - specifier: ^3.3.0 - version: 3.3.0 - '@atcute/client': - specifier: ^4.2.1 - version: 4.2.1 - '@atcute/identity-resolver': - specifier: ^1.2.2 - version: 1.2.2(@atcute/identity@1.1.4) - '@atcute/lex-cli': - specifier: ^2.5.3 - version: 2.5.3 - '@atcute/lexicon-doc': - specifier: ^2.1.2 - version: 2.1.2 - '@atcute/lexicons': - specifier: ^1.2.9 - version: 1.2.9 - '@atcute/oauth-node-client': - specifier: ^1.1.0 - version: 1.1.0 - '@atcute/tid': - specifier: ^1.1.2 - version: 1.1.2 - '@cloudflare/workers-types': - specifier: ^4.20260317.1 - version: 4.20260317.1 - '@eslint/compat': - specifier: ^2.0.3 - version: 2.0.3(eslint@10.1.0(jiti@2.6.1)) - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) - '@sveltejs/adapter-cloudflare': - specifier: ^7.2.8 - version: 7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.78.0(@cloudflare/workers-types@4.20260317.1)) - '@sveltejs/kit': - specifier: ^2.55.0 - version: 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - '@sveltejs/vite-plugin-svelte': - specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - '@tailwindcss/forms': - specifier: ^0.5.11 - version: 0.5.11(tailwindcss@4.2.2) - '@tailwindcss/vite': - specifier: ^4.2.2 - version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - bits-ui: - specifier: ^2.16.4 - version: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - eslint: - specifier: ^10.1.0 - version: 10.1.0(jiti@2.6.1) - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-svelte: - specifier: ^3.16.0 - version: 3.16.0(eslint@10.1.0(jiti@2.6.1))(svelte@5.55.0) - globals: - specifier: ^17.4.0 - version: 17.4.0 - prettier: - specifier: ^3.8.1 - version: 3.8.1 - prettier-plugin-svelte: - specifier: ^3.5.1 - version: 3.5.1(prettier@3.8.1)(svelte@5.55.0) - prettier-plugin-tailwindcss: - specifier: ^0.7.2 - version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.0))(prettier@3.8.1) - svelte: - specifier: ^5.55.0 - version: 5.55.0 - svelte-check: - specifier: ^4.4.5 - version: 4.4.5(picomatch@4.0.4)(svelte@5.55.0)(typescript@6.0.2) - tailwindcss: - specifier: ^4.2.2 - version: 4.2.2 - tsx: - specifier: ^4.21.0 - version: 4.21.0 - typescript: - specifier: ^6.0.2 - version: 6.0.2 - typescript-eslint: - specifier: ^8.57.2 - version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - vite: - specifier: ^8.0.3 - version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - wrangler: - specifier: ^4.78.0 - version: 4.78.0(@cloudflare/workers-types@4.20260317.1) - -packages: - - '@atcute/atproto@3.1.10': - resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==, tarball: https://registry.npmjs.org/@atcute/atproto/-/atproto-3.1.10.tgz} - - '@atcute/bluesky-richtext-segmenter@3.0.0': - resolution: {integrity: sha512-NhZTUKtFpeBBbILwAcxj5u4RobIoHOmGw3CAaaEFNebKYSvmTecrXJ7XufHw5DFOUdr8SiKXQVRQxGAxulMNWg==, tarball: https://registry.npmjs.org/@atcute/bluesky-richtext-segmenter/-/bluesky-richtext-segmenter-3.0.0.tgz} - - '@atcute/bluesky@3.3.0': - resolution: {integrity: sha512-TrLnlxuD6F/D2ZYzJ3aCiRD0yiFuhmVsd6oULNzzr8V9Xzlufg0yxkRiGmbMiF2iI508y/MFi6vzo625301c5A==, tarball: https://registry.npmjs.org/@atcute/bluesky/-/bluesky-3.3.0.tgz} - - '@atcute/car@5.1.1': - resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==, tarball: https://registry.npmjs.org/@atcute/car/-/car-5.1.1.tgz} - - '@atcute/cbor@2.3.2': - resolution: {integrity: sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ==, tarball: https://registry.npmjs.org/@atcute/cbor/-/cbor-2.3.2.tgz} - - '@atcute/cid@2.4.1': - resolution: {integrity: sha512-bwhna69RCv7yetXudtj+2qrMPYvhhIQqvJz6YUpUS98v7OdF3X2dnye9Nig2NDrklZcuyOsu7sQo7GOykJXRLQ==, tarball: https://registry.npmjs.org/@atcute/cid/-/cid-2.4.1.tgz} - - '@atcute/client@4.2.1': - resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==, tarball: https://registry.npmjs.org/@atcute/client/-/client-4.2.1.tgz} - - '@atcute/crypto@2.4.1': - resolution: {integrity: sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==, tarball: https://registry.npmjs.org/@atcute/crypto/-/crypto-2.4.1.tgz} - - '@atcute/identity-resolver@1.2.2': - resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==, tarball: https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.2.2.tgz} - peerDependencies: - '@atcute/identity': ^1.0.0 - - '@atcute/identity@1.1.4': - resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==, tarball: https://registry.npmjs.org/@atcute/identity/-/identity-1.1.4.tgz} - - '@atcute/jetstream@1.1.2': - resolution: {integrity: sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==, tarball: https://registry.npmjs.org/@atcute/jetstream/-/jetstream-1.1.2.tgz} - - '@atcute/lex-cli@2.5.3': - resolution: {integrity: sha512-829rvezMOfRkJQRKvupNT8TWT/YYffJ2QsB80D9aPjkXSogrETZA7xZcPaMZBXg+mJaVbLO9S4ThPQmlF0L4UQ==, tarball: https://registry.npmjs.org/@atcute/lex-cli/-/lex-cli-2.5.3.tgz} - hasBin: true - - '@atcute/lexicon-doc@2.1.2': - resolution: {integrity: sha512-jTLcOka7b8BIn2SnIZm2m7l6unlJ0gpgW1MnRpSqNbly/AvyRUR/GREduh/QmjT4SGasDm8vdhrM0kOSPFpDLQ==, tarball: https://registry.npmjs.org/@atcute/lexicon-doc/-/lexicon-doc-2.1.2.tgz} - - '@atcute/lexicon-resolver@0.1.6': - resolution: {integrity: sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==, tarball: https://registry.npmjs.org/@atcute/lexicon-resolver/-/lexicon-resolver-0.1.6.tgz} - peerDependencies: - '@atcute/identity': ^1.1.0 - '@atcute/identity-resolver': ^1.1.3 - - '@atcute/lexicons@1.2.9': - resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==, tarball: https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.9.tgz} - - '@atcute/mst@1.0.0': - resolution: {integrity: sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==, tarball: https://registry.npmjs.org/@atcute/mst/-/mst-1.0.0.tgz} - - '@atcute/multibase@1.2.0': - resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==, tarball: https://registry.npmjs.org/@atcute/multibase/-/multibase-1.2.0.tgz} - - '@atcute/oauth-crypto@0.1.0': - resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==, tarball: https://registry.npmjs.org/@atcute/oauth-crypto/-/oauth-crypto-0.1.0.tgz} - - '@atcute/oauth-keyset@0.1.0': - resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==, tarball: https://registry.npmjs.org/@atcute/oauth-keyset/-/oauth-keyset-0.1.0.tgz} - - '@atcute/oauth-node-client@1.1.0': - resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==, tarball: https://registry.npmjs.org/@atcute/oauth-node-client/-/oauth-node-client-1.1.0.tgz} - - '@atcute/oauth-types@0.1.1': - resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==, tarball: https://registry.npmjs.org/@atcute/oauth-types/-/oauth-types-0.1.1.tgz} - - '@atcute/repo@0.1.3': - resolution: {integrity: sha512-kN4gkrkQgJwI5xkVQ9zSdI8ULR47uH48EdJdjMvKAWsSDu8zKYd8ZQ6n778qa6o3WYEq02v9QVf7pzeU+3fYVA==, tarball: https://registry.npmjs.org/@atcute/repo/-/repo-0.1.3.tgz} - - '@atcute/tid@1.1.2': - resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==, tarball: https://registry.npmjs.org/@atcute/tid/-/tid-1.1.2.tgz} - - '@atcute/time-ms@1.3.2': - resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==, tarball: https://registry.npmjs.org/@atcute/time-ms/-/time-ms-1.3.2.tgz} - - '@atcute/uint8array@1.1.1': - resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==, tarball: https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.1.1.tgz} - - '@atcute/util-fetch@1.0.5': - resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==, tarball: https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.5.tgz} - - '@atcute/util-text@1.2.0': - resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==, tarball: https://registry.npmjs.org/@atcute/util-text/-/util-text-1.2.0.tgz} - - '@atcute/varint@2.0.0': - resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==, tarball: https://registry.npmjs.org/@atcute/varint/-/varint-2.0.0.tgz} - - '@badrap/valita@0.4.6': - resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==, tarball: https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz} - engines: {node: '>= 18'} - - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==, tarball: https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz} - engines: {node: '>=18.0.0'} - - '@cloudflare/unenv-preset@2.16.0': - resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==, tarball: https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/workerd-darwin-64@1.20260317.1': - resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260317.1': - resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20260317.1': - resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260317.1': - resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-windows-64@1.20260317.1': - resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cloudflare/workers-types@4.20260317.1': - resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz} - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, tarball: https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz} - engines: {node: '>=12'} - - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz} - - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==, tarball: https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==, tarball: https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz} - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, tarball: https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, tarball: https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/compat@2.0.3': - resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==, tarball: https://registry.npmjs.org/@eslint/compat/-/compat-2.0.3.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^8.40 || 9 || 10 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==, tarball: https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==, tarball: https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==, tarball: https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==, tarball: https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==, tarball: https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==, tarball: https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz} - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==, tarball: https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz} - - '@foxui/core@0.8.2': - resolution: {integrity: sha512-cVfFCQX4SfL7mezfQHAHln8M+ZiEZlwa4lIXMFUATLuhl4YCcwv7IvvtU+hMvq2ky+vsCEEry1XvXOLdSe6zAA==, tarball: https://registry.npmjs.org/@foxui/core/-/core-0.8.2.tgz} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/social@0.8.4': - resolution: {integrity: sha512-+5pV402gpEwxSdWb0EMr2SKG/kutCEXzQI930oGA6Q8O/Dnk14noCf/hYAtZvBlAd0cwa6Grlgt7/VEwzU5NDg==, tarball: https://registry.npmjs.org/@foxui/social/-/social-0.8.4.tgz} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/text@0.8.2': - resolution: {integrity: sha512-f1WotRAzZszqcg/c0RBs9Wuy7KdyHhsZRJNBaufFLl+hzMhCSJQq11lQPdRCUITKP2rBfeguZfqkAcqi0PuI8w==, tarball: https://registry.npmjs.org/@foxui/text/-/text-0.8.2.tgz} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@foxui/time@0.8.2': - resolution: {integrity: sha512-w60jJij44bcle905AL/vBijyI/ilY4cSA21yxYg8C6swm5uH5wooW1fSFaTuC/Lg0GfhwTkW963kz66vVsYbiw==, tarball: https://registry.npmjs.org/@foxui/time/-/time-0.8.2.tgz} - peerDependencies: - svelte: '>=5' - tailwindcss: '>=3' - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, tarball: https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, tarball: https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, tarball: https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, tarball: https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz} - engines: {node: '>=18.18'} - - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==, tarball: https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, tarball: https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, tarball: https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, tarball: https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, tarball: https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz} - cpu: [riscv64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, tarball: https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, tarball: https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, tarball: https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, tarball: https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, tarball: https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, tarball: https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, tarball: https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, tarball: https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, tarball: https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, tarball: https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, tarball: https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, tarball: https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@internationalized/date@3.10.1': - resolution: {integrity: sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==, tarball: https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, tarball: https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz} - - '@mary-ext/event-iterator@1.0.0': - resolution: {integrity: sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==, tarball: https://registry.npmjs.org/@mary-ext/event-iterator/-/event-iterator-1.0.0.tgz} - - '@mary-ext/simple-event-emitter@1.0.1': - resolution: {integrity: sha512-9+VvZisxZ/gSg+JJH7hmXaA8Qj42Qjz3O58RSB+INYc8iLA0icATZxHB9vKbj59ojDGZjO3hCKzMXocx3L0H8w==, tarball: https://registry.npmjs.org/@mary-ext/simple-event-emitter/-/simple-event-emitter-1.0.1.tgz} - - '@napi-rs/wasm-runtime@1.1.2': - resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==, tarball: https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@noble/secp256k1@3.0.0': - resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==, tarball: https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz} - - '@number-flow/svelte@0.4.0': - resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==, tarball: https://registry.npmjs.org/@number-flow/svelte/-/svelte-0.4.0.tgz} - peerDependencies: - svelte: ^4 || ^5 - - '@optique/core@0.6.11': - resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==, tarball: https://registry.npmjs.org/@optique/core/-/core-0.6.11.tgz} - engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} - - '@optique/run@0.6.11': - resolution: {integrity: sha512-tsXBEygGSzNpFK2gjsRlXBn7FiScUeLFWIZNpoAZ8iG85Km0/3K9xgqlQAXoQ+uEZBe4XplnzyCDvmEgbyNT8w==, tarball: https://registry.npmjs.org/@optique/run/-/run-0.6.11.tgz} - engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} - - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==, tarball: https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz} - - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, tarball: https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz} - - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==, tarball: https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz} - - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==, tarball: https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==, tarball: https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz} - - '@remirror/core-constants@3.0.0': - resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==, tarball: https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz} - - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==, tarball: https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==, tarball: https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==, tarball: https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==, tarball: https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==, tarball: https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz} - - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==, tarball: https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz} - engines: {node: '>=18'} - - '@speed-highlight/core@1.2.15': - resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==, tarball: https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz} - - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==, tarball: https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz} - peerDependencies: - acorn: ^8.9.0 - - '@sveltejs/adapter-cloudflare@7.2.8': - resolution: {integrity: sha512-bIdhY/Fi4AQmqiBdQVKnafH1h9Gw+xbCvHyUu4EouC8rJOU02zwhi14k/FDhQ0mJF1iblIu3m8UNQ8GpGIvIOQ==, tarball: https://registry.npmjs.org/@sveltejs/adapter-cloudflare/-/adapter-cloudflare-7.2.8.tgz} - peerDependencies: - '@sveltejs/kit': ^2.0.0 - wrangler: ^4.0.0 - - '@sveltejs/kit@2.55.0': - resolution: {integrity: sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==, tarball: https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz} - engines: {node: '>=18.13'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.0.0 - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 - vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - typescript: - optional: true - - '@sveltejs/vite-plugin-svelte@7.0.0': - resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==, tarball: https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.46.4 - vite: ^8.0.0-beta.7 || ^8.0.0 - - '@swc/helpers@0.5.20': - resolution: {integrity: sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==, tarball: https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz} - - '@tailwindcss/forms@0.5.11': - resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==, tarball: https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz} - peerDependencies: - tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - - '@tailwindcss/node@4.2.2': - resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==, tarball: https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz} - - '@tailwindcss/oxide-android-arm64@4.2.2': - resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.2.2': - resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.2.2': - resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.2.2': - resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==, tarball: https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.2.2': - resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==, tarball: https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.2.2': - resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==, tarball: https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 - - '@tiptap/core@3.21.0': - resolution: {integrity: sha512-IfnQiuEeabDSPr1C/zHFTbnvlTf5z0DE/d/xz4C6bkL4ZBDJ3rr99h2qsaV0l8F+kbNswZMlQdM8rxNlMy95fQ==, tarball: https://registry.npmjs.org/@tiptap/core/-/core-3.21.0.tgz} - peerDependencies: - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-blockquote@3.21.0': - resolution: {integrity: sha512-JDM/RR6rM0dMCZ1UnEf7eqmN6pAdIa2llhN+E24HdTGNJCklMFhLAGE/OT8/1r7M0WWA9GVO7/PTe4EdGh6+lQ==, tarball: https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-bold@3.21.0': - resolution: {integrity: sha512-iyEJRzG7XTCPlHwEDzUw3HnuYYCfL7lNpcCHmxcpYMrIUA8rv7EUxerIwApT6xY8hQ/07ljuJKgOyPvnJOOzuA==, tarball: https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-bubble-menu@3.21.0': - resolution: {integrity: sha512-/fabRRhhf8i4LAx9e8xz9ppqN5KgdJk3TxMuxAD5vAWGsejvhSoPa8O8H/QwwyntXm1Vue8aQiMHsUk48b2hGQ==, tarball: https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-bullet-list@3.21.0': - resolution: {integrity: sha512-PWNF+xwxgOeXYGD88sCQLKL0eBoQqjUnZNALxBjN3Y7x4llalh42rHOp2Nt2t6UbQgqTBtBzU/uFcussTpxreQ==, tarball: https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.21.0.tgz} - peerDependencies: - '@tiptap/extension-list': ^3.21.0 - - '@tiptap/extension-code-block-lowlight@3.21.0': - resolution: {integrity: sha512-79sS0tqoGVX6wq30ejzohpUVLeGOkTTUn5hCqjsniyYPTEtrn4tHyBnS4Du2TbrDV4SqcwgisWExhuB8pfEdYQ==, tarball: https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/extension-code-block': ^3.21.0 - '@tiptap/pm': ^3.21.0 - highlight.js: ^11 - lowlight: ^2 || ^3 - - '@tiptap/extension-code-block@3.21.0': - resolution: {integrity: sha512-zrVOcOzDCjHQ8NJcC+qHmZZKiwnP/NMSb3qVJlSMN8TzuHept1MZCDa2Mbo70O6I0txo456SGuXB9sqV1vHmGg==, tarball: https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-code@3.21.0': - resolution: {integrity: sha512-D7wA9jp+4X2r1f3FIoga73s6Rn4rmZY57Jes6a4rK3HY+3yHk1r057pPIZSY8Drfs97jxHQVFdfUYUomLSFYBA==, tarball: https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-document@3.21.0': - resolution: {integrity: sha512-7oCyzXI9ChvJQUlr23AURdfVar4OIsrYUvqdhEwo3bjcI/Q/j0KJiXfuh6ZzL5eVaINSailH53sZaGg4THQtUg==, tarball: https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-dropcursor@3.21.0': - resolution: {integrity: sha512-6fsDSVAM2iz7eElvT6iivMrGBGjIP/oPigVZ/SPm6f31phaYhz6TIOEgV/Lr2jaPIOgyK4U0cU4Yd4KUBCmhzQ==, tarball: https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.21.0.tgz} - peerDependencies: - '@tiptap/extensions': ^3.21.0 - - '@tiptap/extension-floating-menu@3.21.0': - resolution: {integrity: sha512-n2HzTB+I/5rAl8R/1sKMv92JiY1oDK1hroXizxEKYa6dskJcAMW0CfYyPcPOZWQQEe7qoeOvQISr2ooLAKW+Mw==, tarball: https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.21.0.tgz} - peerDependencies: - '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-gapcursor@3.21.0': - resolution: {integrity: sha512-wGjgAoYBTvPAe9QYMI5px355XcNeMkaUrMY9IHbMqgqdmHcDxqooxM4H6sYVX2CRcHwXy4I8NQAoOhSYrQJDMg==, tarball: https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.21.0.tgz} - peerDependencies: - '@tiptap/extensions': ^3.21.0 - - '@tiptap/extension-hard-break@3.21.0': - resolution: {integrity: sha512-6JFVSAOQ1qhQHi9mVcdn2/XO8YIMgYV8zjarzNUzP6Sf2waeE5BLXjlg6rIH/945sY1J+FndTojLru6gQ07a5A==, tarball: https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-heading@3.21.0': - resolution: {integrity: sha512-ji6VJmoRnDzAHYflEYEZohMHRi77UGLW1o3ua7UhI32iJ9nuYssbPNuzEeE4SvENMQwZRszad5+a+dKAa+NC7g==, tarball: https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-history@3.21.0': - resolution: {integrity: sha512-aYUxm0fLXGfn4XAsRq53lOX1cB+GRF8rScVuDHmQfEaJHNo09DSRtvgWnDHVdqIS8FvKkBcTO8eyPOBPMvkWiA==, tarball: https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-3.21.0.tgz} - peerDependencies: - '@tiptap/extensions': ^3.21.0 - - '@tiptap/extension-horizontal-rule@3.21.0': - resolution: {integrity: sha512-vNBnOfFEY62CoJPGo4nonRM7RiOvhII1vhoO+WFr1GxDqCAfmEFjToflt7JT1UJdo6lMVcD+aaaAgOiuSz5p6g==, tarball: https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-image@3.21.0': - resolution: {integrity: sha512-W9786a2K4LSZJMPeRLmoDulJeXOsM0ueRV2MHjTol7ikPRauROB7GUbAz9DyPAJHA2AGUfpswnGAYPO3tz5CLg==, tarball: https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-italic@3.21.0': - resolution: {integrity: sha512-2I8oPvwyXhRn1k8lbDFIutzvhtLEjoO5mmQCNX4TnT4PdxxaSrK9+ihYg12VeqhUeO7dg1MKiFqws0HVBrwzWg==, tarball: https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-link@3.21.0': - resolution: {integrity: sha512-oMU7Yve1sbgBsaFAUc2R0GPf4d3ZPVJeMUFC6b6X9rJIvx/IhEUEn9toQcSBGfp02uWK9NdQyIFYFdWlVXH++w==, tarball: https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-list-item@3.21.0': - resolution: {integrity: sha512-1ZymZmlQVbAoC4q5x3cro0v5+3I6l+BHqbhIMQLjQFlAOJfcE0pvqRzAFW7PduxUj41tXEtsYqp2NREvO9F5Fg==, tarball: https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.21.0.tgz} - peerDependencies: - '@tiptap/extension-list': ^3.21.0 - - '@tiptap/extension-list-keymap@3.21.0': - resolution: {integrity: sha512-EzrfW3ASNFPWKhR8sNOq7Kqw4hvaTAOn4dlI7chB8HIANSrlyPOUn+eKAnO6HQgsUgsbjg2GbTUrGrxcoLykUg==, tarball: https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.21.0.tgz} - peerDependencies: - '@tiptap/extension-list': ^3.21.0 - - '@tiptap/extension-list@3.21.0': - resolution: {integrity: sha512-KeBlEtLrGce2d3dgL89hmwWEtREuzlW4XY5bYWpKNvCbFqvdSb3n7vkdkw32YclZmMWxAcABgW6ucCStkE0rsQ==, tarball: https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/extension-ordered-list@3.21.0': - resolution: {integrity: sha512-+d+0orokMfqaBfvr9tUBgGvo2ZCV+fR3JzsJTmnLBWOkhBSJN7H4pnfXPTue0qwspUwRmkLJxdIlU+J7HkMrng==, tarball: https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.21.0.tgz} - peerDependencies: - '@tiptap/extension-list': ^3.21.0 - - '@tiptap/extension-paragraph@3.21.0': - resolution: {integrity: sha512-cMPG/jCoZ9NmLZ5ctFziILaxJGfDtMTb5OLBhifMFZeMVwF1pEJIygDEfnX/HSruv507weZSQG4pERO2tRszMg==, tarball: https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-placeholder@3.21.0': - resolution: {integrity: sha512-fs+cQqMh1d1naV6OgOhP/0qbRJwtw8DpQMj3/oqGKbaRRKIeecEaZPXYRd7MYa4e9K0Cfk5Bm0MNs9lwu/BYsw==, tarball: https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.21.0.tgz} - peerDependencies: - '@tiptap/extensions': ^3.21.0 - - '@tiptap/extension-strike@3.21.0': - resolution: {integrity: sha512-easnVaN11Wl+5fOtfvzJ10J762S9TRXZaMj5rLBGavgf82DCYHqhGhBqpLQrJ41r4nPABGlYvTRoxfvBLB74Lg==, tarball: https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-text@3.21.0': - resolution: {integrity: sha512-Zx8QdB8a5iBuE4uO21c3BjmpBfaJEr2Jd1QFnsdgx11fm6P7dGgZaGko1FaINhfOPRGTN6O/kiF02cDMdOHa/w==, tarball: https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-typography@3.21.0': - resolution: {integrity: sha512-/nHtkQTtBPmQPe4JnEGBxxCKIv2FToSB3PUfh7b4cccNkbs7yJZO30ySNJcMzNfn8pfSQPkJxZJcFuSm9RjZdg==, tarball: https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extension-underline@3.21.0': - resolution: {integrity: sha512-gGmBEymbWnr8AIS8bI/bPw5rcwo7wAFcBw/TsLd1nAanu1dDqSRNDBrit3m02Ru+D88u2SfNvmbOPI1pz+1f5w==, tarball: https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - - '@tiptap/extensions@3.21.0': - resolution: {integrity: sha512-MN1uh5PmHT1F2BNsbc21MIS0AMFFA73oODlp/4ckpBR4o5AxRwV+8f43Cd52UL4MgMkKj/A+QfZ7iK9IDb0h5A==, tarball: https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/markdown@3.21.0': - resolution: {integrity: sha512-VhnHhB9LeEfB2xnbNZUV2itK0ae4iERbr+a4/ZrlZB9auPtggW4fIob/bQGOaeU+GZtk1xDLyKUDyKEEwgTXMw==, tarball: https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tiptap/pm@3.21.0': - resolution: {integrity: sha512-I3sNo7oMMsR6FFz1ecvPb9uCF0VQuS2WV67j8Io2M7DJicRWCE/GM5DaiYjTeWBbnByk6BuG0txoJATAqPVliQ==, tarball: https://registry.npmjs.org/@tiptap/pm/-/pm-3.21.0.tgz} - - '@tiptap/starter-kit@3.21.0': - resolution: {integrity: sha512-w7fWxglDtqXFBgRYH+LforJyUboSAQllnWQbGVSTyX4rsICqZjkb3f6CTSUWpGoGKmlmbb2ZpEuoik7tur9d8Q==, tarball: https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.21.0.tgz} - - '@tiptap/suggestion@3.21.0': - resolution: {integrity: sha512-ShgbaH2hbCRLV+AaVuSx7a/nzPOSdUeW0TNeuxZfWim62MBcx33qtllja1zOoY2tgGXokEihuzFODx83IyvGfQ==, tarball: https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.21.0.tgz} - peerDependencies: - '@tiptap/core': ^3.21.0 - '@tiptap/pm': ^3.21.0 - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, tarball: https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz} - - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==, tarball: https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz} - - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, tarball: https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==, tarball: https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, tarball: https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==, tarball: https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==, tarball: https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==, tarball: https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz} - - '@types/node@25.0.10': - resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==, tarball: https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz} - - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, tarball: https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==, tarball: https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz} - - '@typescript-eslint/eslint-plugin@8.57.2': - resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.57.2 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.57.2': - resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.57.2': - resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==, tarball: https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.57.2': - resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.57.2': - resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==, tarball: https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.57.2': - resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.57.2': - resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.57.2': - resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.57.2': - resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.57.2': - resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@use-gesture/core@10.3.1': - resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==, tarball: https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz} - - '@use-gesture/vanilla@10.3.1': - resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==, tarball: https://registry.npmjs.org/@use-gesture/vanilla/-/vanilla-10.3.1.tgz} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==, tarball: https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz} - - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==, tarball: https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz} - engines: {node: '>= 0.4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, tarball: https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz} - engines: {node: '>= 0.4'} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, tarball: https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz} - engines: {node: 18 || 20 || >=22} - - bits-ui@2.16.4: - resolution: {integrity: sha512-76dKbW0DGplG7qnVM3kljE2CmCXqh9tj9OfVo0DvC/ZGRkZNJ/auU/aQnMKBxDnSqRvsz2mZBccBkUKyF4/SoQ==, tarball: https://registry.npmjs.org/bits-ui/-/bits-ui-2.16.4.tgz} - engines: {node: '>=20'} - peerDependencies: - '@internationalized/date': ^3.8.1 - svelte: ^5.33.0 - - blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==, tarball: https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz} - - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz} - engines: {node: 18 || 20 || >=22} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz} - engines: {node: '>= 14.16.0'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, tarball: https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz} - engines: {node: '>=6'} - - contrail@https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf: - resolution: {tarball: https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf} - version: 0.0.2 - - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz} - engines: {node: '>= 0.6'} - - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, tarball: https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz} - engines: {node: '>=18'} - - core-js@3.49.0: - resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==, tarball: https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz} - - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==, tarball: https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz} - engines: {node: '>= 8'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, tarball: https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz} - engines: {node: '>=4'} - hasBin: true - - custom-event-polyfill@1.0.7: - resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==, tarball: https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, tarball: https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, tarball: https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz} - engines: {node: '>=0.10.0'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, tarball: https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz} - engines: {node: '>=6'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz} - engines: {node: '>=8'} - - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==, tarball: https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==, tarball: https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz} - - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==, tarball: https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz} - - emoji-picker-element@1.29.1: - resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==, tarball: https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.29.1.tgz} - - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz} - engines: {node: '>=10.13.0'} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, tarball: https://registry.npmjs.org/entities/-/entities-4.5.0.tgz} - engines: {node: '>=0.12'} - - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, tarball: https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz} - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, tarball: https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz} - engines: {node: '>=10'} - - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==, tarball: https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-svelte@3.16.0: - resolution: {integrity: sha512-DJXxqpYZUxcE0SfYo8EJzV2ZC+zAD7fJp1n1HwcEMRR1cOEUYvjT9GuzJeNghMjgb7uxuK3IJAzI+x6zzUxO5A==, tarball: https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.16.0.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, tarball: https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==, tarball: https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.1.0: - resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==, tarball: https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==, tarball: https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz} - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, tarball: https://registry.npmjs.org/espree/-/espree-10.4.0.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==, tarball: https://registry.npmjs.org/espree/-/espree-11.2.0.tgz} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, tarball: https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz} - engines: {node: '>=0.10'} - - esrap@2.2.4: - resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==, tarball: https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, tarball: https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, tarball: https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, tarball: https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz} - engines: {node: '>=0.10.0'} - - event-target-polyfill@0.0.4: - resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==, tarball: https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, tarball: https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, tarball: https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, tarball: https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, tarball: https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, tarball: https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==, tarball: https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, tarball: https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==, tarball: https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz} - engines: {node: '>=10.13.0'} - - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==, tarball: https://registry.npmjs.org/globals/-/globals-16.5.0.tgz} - engines: {node: '>=18'} - - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==, tarball: https://registry.npmjs.org/globals/-/globals-17.4.0.tgz} - engines: {node: '>=18'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, tarball: https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz} - - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==, tarball: https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz} - engines: {node: '>=12.0.0'} - - hls.js@1.6.15: - resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==, tarball: https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz} - - hono@4.12.9: - resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==, tarball: https://registry.npmjs.org/hono/-/hono-4.12.9.tgz} - engines: {node: '>=16.9.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, tarball: https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==, tarball: https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, tarball: https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz} - engines: {node: '>=0.8.19'} - - inline-style-parser@0.2.7: - resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==, tarball: https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz} - - is-emoji-supported@0.0.5: - resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==, tarball: https://registry.npmjs.org/is-emoji-supported/-/is-emoji-supported-0.0.5.tgz} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, tarball: https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz} - engines: {node: '>=0.10.0'} - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==, tarball: https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==, tarball: https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, tarball: https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, tarball: https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz} - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, tarball: https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz} - engines: {node: '>=6'} - - known-css-properties@0.37.0: - resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==, tarball: https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmjs.org/levn/-/levn-0.4.1.tgz} - engines: {node: '>= 0.8.0'} - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, tarball: https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, tarball: https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, tarball: https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, tarball: https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, tarball: https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, tarball: https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, tarball: https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, tarball: https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, tarball: https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, tarball: https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz} - engines: {node: '>= 12.0.0'} - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz} - engines: {node: '>=10'} - - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==, tarball: https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz} - - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==, tarball: https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz} - - loadjs@4.3.0: - resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==, tarball: https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==, tarball: https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, tarball: https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz} - engines: {node: '>=10'} - - lowlight@3.3.0: - resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==, tarball: https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, tarball: https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz} - hasBin: true - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz} - - markdown-it@14.1.1: - resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==, tarball: https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz} - hasBin: true - - marked@17.0.5: - resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==, tarball: https://registry.npmjs.org/marked/-/marked-17.0.5.tgz} - engines: {node: '>= 20'} - hasBin: true - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==, tarball: https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz} - - mini-svg-data-uri@1.4.4: - resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==, tarball: https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz} - hasBin: true - - miniflare@4.20260317.3: - resolution: {integrity: sha512-tK78D3X4q30/SXqVwMhWrUfH+ffRou9dJLC+jkhNy5zh1I7i7T4JH6xihOvYxdCSBavJ5fQXaaxDJz6orh09BA==, tarball: https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.3.tgz} - engines: {node: '>=18.0.0'} - hasBin: true - - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz} - engines: {node: 18 || 20 || >=22} - - mode-watcher@1.1.0: - resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==, tarball: https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz} - peerDependencies: - svelte: ^5.27.0 - - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, tarball: https://registry.npmjs.org/mri/-/mri-1.2.0.tgz} - engines: {node: '>=4'} - - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, tarball: https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@5.1.7: - resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz} - engines: {node: ^18 || >=20} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, tarball: https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz} - - number-flow@0.6.0: - resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==, tarball: https://registry.npmjs.org/number-flow/-/number-flow-0.6.0.tgz} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, tarball: https://registry.npmjs.org/obug/-/obug-2.1.1.tgz} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, tarball: https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz} - engines: {node: '>= 0.8.0'} - - orderedmap@2.1.1: - resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==, tarball: https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, tarball: https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, tarball: https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz} - engines: {node: '>=10'} - - partysocket@1.1.16: - resolution: {integrity: sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w==, tarball: https://registry.npmjs.org/partysocket/-/partysocket-1.1.16.tgz} - peerDependencies: - react: '>=17' - peerDependenciesMeta: - react: - optional: true - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} - engines: {node: '>=8'} - - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, tarball: https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz} - engines: {node: '>=12'} - - plyr@3.8.4: - resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==, tarball: https://registry.npmjs.org/plyr/-/plyr-3.8.4.tgz} - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==, tarball: https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-safe-parser@7.0.1: - resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==, tarball: https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz} - engines: {node: '>=18.0'} - peerDependencies: - postcss: ^8.4.31 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==, tarball: https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==, tarball: https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz} - engines: {node: '>=4'} - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, tarball: https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz} - engines: {node: '>= 0.8.0'} - - prettier-plugin-svelte@3.5.1: - resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==, tarball: https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - - prettier-plugin-tailwindcss@0.7.2: - resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==, tarball: https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz} - engines: {node: '>=20.19'} - peerDependencies: - '@ianvs/prettier-plugin-sort-imports': '*' - '@prettier/plugin-hermes': '*' - '@prettier/plugin-oxc': '*' - '@prettier/plugin-pug': '*' - '@shopify/prettier-plugin-liquid': '*' - '@trivago/prettier-plugin-sort-imports': '*' - '@zackad/prettier-plugin-twig': '*' - prettier: ^3.0 - prettier-plugin-astro: '*' - prettier-plugin-css-order: '*' - prettier-plugin-jsdoc: '*' - prettier-plugin-marko: '*' - prettier-plugin-multiline-arrays: '*' - prettier-plugin-organize-attributes: '*' - prettier-plugin-organize-imports: '*' - prettier-plugin-sort-imports: '*' - prettier-plugin-svelte: '*' - peerDependenciesMeta: - '@ianvs/prettier-plugin-sort-imports': - optional: true - '@prettier/plugin-hermes': - optional: true - '@prettier/plugin-oxc': - optional: true - '@prettier/plugin-pug': - optional: true - '@shopify/prettier-plugin-liquid': - optional: true - '@trivago/prettier-plugin-sort-imports': - optional: true - '@zackad/prettier-plugin-twig': - optional: true - prettier-plugin-astro: - optional: true - prettier-plugin-css-order: - optional: true - prettier-plugin-jsdoc: - optional: true - prettier-plugin-marko: - optional: true - prettier-plugin-multiline-arrays: - optional: true - prettier-plugin-organize-attributes: - optional: true - prettier-plugin-organize-imports: - optional: true - prettier-plugin-sort-imports: - optional: true - prettier-plugin-svelte: - optional: true - - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, tarball: https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz} - engines: {node: '>=14'} - hasBin: true - - prosemirror-changeset@2.4.0: - resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==, tarball: https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz} - - prosemirror-collab@1.3.1: - resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==, tarball: https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz} - - prosemirror-commands@1.7.1: - resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==, tarball: https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz} - - prosemirror-dropcursor@1.8.2: - resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==, tarball: https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz} - - prosemirror-gapcursor@1.4.1: - resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==, tarball: https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz} - - prosemirror-history@1.5.0: - resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==, tarball: https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz} - - prosemirror-inputrules@1.5.1: - resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==, tarball: https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz} - - prosemirror-keymap@1.2.3: - resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==, tarball: https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz} - - prosemirror-markdown@1.13.4: - resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==, tarball: https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz} - - prosemirror-menu@1.3.0: - resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==, tarball: https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz} - - prosemirror-model@1.25.4: - resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==, tarball: https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz} - - prosemirror-schema-basic@1.2.4: - resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==, tarball: https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz} - - prosemirror-schema-list@1.5.1: - resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==, tarball: https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz} - - prosemirror-state@1.4.4: - resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==, tarball: https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz} - - prosemirror-tables@1.8.5: - resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==, tarball: https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz} - - prosemirror-trailing-node@3.0.0: - resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==, tarball: https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz} - peerDependencies: - prosemirror-model: ^1.22.1 - prosemirror-state: ^1.4.2 - prosemirror-view: ^1.33.8 - - prosemirror-transform@1.11.0: - resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==, tarball: https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz} - - prosemirror-view@1.41.7: - resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==, tarball: https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==, tarball: https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz} - engines: {node: '>=6'} - - rangetouch@2.0.1: - resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==, tarball: https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz} - engines: {node: '>= 14.18.0'} - - regexparam@3.0.0: - resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==, tarball: https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz} - engines: {node: '>=8'} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, tarball: https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz} - - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==, tarball: https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==, tarball: https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz} - - runed@0.23.4: - resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==, tarball: https://registry.npmjs.org/runed/-/runed-0.23.4.tgz} - peerDependencies: - svelte: ^5.7.0 - - runed@0.25.0: - resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==, tarball: https://registry.npmjs.org/runed/-/runed-0.25.0.tgz} - peerDependencies: - svelte: ^5.7.0 - - runed@0.28.0: - resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==, tarball: https://registry.npmjs.org/runed/-/runed-0.28.0.tgz} - peerDependencies: - svelte: ^5.7.0 - - runed@0.35.1: - resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==, tarball: https://registry.npmjs.org/runed/-/runed-0.35.1.tgz} - peerDependencies: - '@sveltejs/kit': ^2.21.0 - svelte: ^5.7.0 - peerDependenciesMeta: - '@sveltejs/kit': - optional: true - - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==, tarball: https://registry.npmjs.org/sade/-/sade-1.8.1.tgz} - engines: {node: '>=6'} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, tarball: https://registry.npmjs.org/semver/-/semver-7.7.4.tgz} - engines: {node: '>=10'} - hasBin: true - - set-cookie-parser@3.1.0: - resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==, tarball: https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz} - - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, tarball: https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} - engines: {node: '>=8'} - - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, tarball: https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz} - engines: {node: '>=18'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz} - engines: {node: '>=0.10.0'} - - style-to-object@1.0.14: - resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==, tarball: https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz} - - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==, tarball: https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz} - engines: {node: '>=18'} - - svelte-check@4.4.5: - resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==, tarball: https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' - - svelte-eslint-parser@1.6.0: - resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==, tarball: https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-sonner@1.1.0: - resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==, tarball: https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.0.tgz} - peerDependencies: - svelte: ^5.0.0 - - svelte-tiptap@3.0.1: - resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==, tarball: https://registry.npmjs.org/svelte-tiptap/-/svelte-tiptap-3.0.1.tgz} - peerDependencies: - '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.0.0 - '@tiptap/extension-bubble-menu': ^3.0.0 - '@tiptap/extension-floating-menu': ^3.0.0 - '@tiptap/pm': ^3.0.0 - svelte: ^5.0.0 - - svelte-toolbelt@0.10.6: - resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==, tarball: https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz} - engines: {node: '>=18', pnpm: '>=8.7.0'} - peerDependencies: - svelte: ^5.30.2 - - svelte-toolbelt@0.7.1: - resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==, tarball: https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz} - engines: {node: '>=18', pnpm: '>=8.7.0'} - peerDependencies: - svelte: ^5.0.0 - - svelte@5.55.0: - resolution: {integrity: sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==, tarball: https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz} - engines: {node: '>=18'} - - tabbable@6.4.0: - resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==, tarball: https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz} - - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==, tarball: https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz} - - tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==, tarball: https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz} - engines: {node: '>=16.x', pnpm: '>=7.x'} - peerDependencies: - tailwind-merge: '>=3.0.0' - tailwindcss: '*' - peerDependenciesMeta: - tailwind-merge: - optional: true - - tailwindcss@4.2.2: - resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==, tarball: https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz} - - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==, tarball: https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz} - engines: {node: '>=6'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz} - engines: {node: '>=12.0.0'} - - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, tarball: https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz} - engines: {node: '>=6'} - - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, tarball: https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==, tarball: https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz} - engines: {node: '>=18.0.0'} - hasBin: true - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, tarball: https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz} - engines: {node: '>= 0.8.0'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, tarball: https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz} - engines: {node: '>=16'} - - typescript-eslint@8.57.2: - resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==, tarball: https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - typescript@6.0.2: - resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==, tarball: https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz} - engines: {node: '>=14.17'} - hasBin: true - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==, tarball: https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz} - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz} - - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==, tarball: https://registry.npmjs.org/undici/-/undici-7.24.4.tgz} - engines: {node: '>=20.18.1'} - - unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, tarball: https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz} - - unicode-segmenter@0.14.5: - resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==, tarball: https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, tarball: https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz} - - url-polyfill@1.1.14: - resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==, tarball: https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, tarball: https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz} - - valibot@1.3.1: - resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==, tarball: https://registry.npmjs.org/valibot/-/valibot-1.3.1.tgz} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - - vite@8.0.3: - resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==, tarball: https://registry.npmjs.org/vite/-/vite-8.0.3.tgz} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==, tarball: https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, tarball: https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz} - - web-haptics@0.0.6: - resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==, tarball: https://registry.npmjs.org/web-haptics/-/web-haptics-0.0.6.tgz} - peerDependencies: - react: '>=18' - react-dom: '>=18' - svelte: '>=4' - vue: '>=3' - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - svelte: - optional: true - vue: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, tarball: https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz} - engines: {node: '>=0.10.0'} - - workerd@1.20260317.1: - resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==, tarball: https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz} - engines: {node: '>=16'} - hasBin: true - - worktop@0.8.0-next.18: - resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==, tarball: https://registry.npmjs.org/worktop/-/worktop-0.8.0-next.18.tgz} - engines: {node: '>=12'} - - wrangler@4.78.0: - resolution: {integrity: sha512-He/vUhk4ih0D0eFmtNnlbT6Od8j+BEokaSR+oYjbVsH0SWIrIch+eHqfLRSBjBQaOoh6HCNxcafcIkBm2u0Hag==, tarball: https://registry.npmjs.org/wrangler/-/wrangler-4.78.0.tgz} - engines: {node: '>=20.3.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260317.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==, tarball: https://registry.npmjs.org/ws/-/ws-8.18.0.tgz} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - yaml@1.10.3: - resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==, tarball: https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz} - engines: {node: '>= 6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz} - engines: {node: '>=10'} - - yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz} - engines: {node: '>=12.20'} - - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==, tarball: https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz} - - youch@4.1.0-beta.10: - resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==, tarball: https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz} - - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==, tarball: https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz} - -snapshots: - - '@atcute/atproto@3.1.10': - dependencies: - '@atcute/lexicons': 1.2.9 - - '@atcute/bluesky-richtext-segmenter@3.0.0': {} - - '@atcute/bluesky@3.3.0': - dependencies: - '@atcute/atproto': 3.1.10 - '@atcute/lexicons': 1.2.9 - - '@atcute/car@5.1.1': - dependencies: - '@atcute/cbor': 2.3.2 - '@atcute/cid': 2.4.1 - '@atcute/uint8array': 1.1.1 - '@atcute/varint': 2.0.0 - - '@atcute/cbor@2.3.2': - dependencies: - '@atcute/cid': 2.4.1 - '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 - - '@atcute/cid@2.4.1': - dependencies: - '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 - - '@atcute/client@4.2.1': - dependencies: - '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 - - '@atcute/crypto@2.4.1': - dependencies: - '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 - '@noble/secp256k1': 3.0.0 - - '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': - dependencies: - '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 - '@atcute/util-fetch': 1.0.5 - '@badrap/valita': 0.4.6 - - '@atcute/identity@1.1.4': - dependencies: - '@atcute/lexicons': 1.2.9 - '@badrap/valita': 0.4.6 - - '@atcute/jetstream@1.1.2': - dependencies: - '@atcute/lexicons': 1.2.9 - '@badrap/valita': 0.4.6 - '@mary-ext/event-iterator': 1.0.0 - '@mary-ext/simple-event-emitter': 1.0.1 - partysocket: 1.1.16 - type-fest: 4.41.0 - yocto-queue: 1.2.2 - transitivePeerDependencies: - - react - - '@atcute/lex-cli@2.5.3': - dependencies: - '@atcute/identity': 1.1.4 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicon-doc': 2.1.2 - '@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4) - '@atcute/lexicons': 1.2.9 - '@badrap/valita': 0.4.6 - '@optique/core': 0.6.11 - '@optique/run': 0.6.11 - picocolors: 1.1.1 - prettier: 3.8.1 - - '@atcute/lexicon-doc@2.1.2': - dependencies: - '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 - '@atcute/uint8array': 1.1.1 - '@atcute/util-text': 1.2.0 - '@badrap/valita': 0.4.6 - - '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4)': - dependencies: - '@atcute/crypto': 2.4.1 - '@atcute/identity': 1.1.4 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicon-doc': 2.1.2 - '@atcute/lexicons': 1.2.9 - '@atcute/repo': 0.1.3 - '@atcute/util-fetch': 1.0.5 - '@badrap/valita': 0.4.6 - - '@atcute/lexicons@1.2.9': - dependencies: - '@atcute/uint8array': 1.1.1 - '@atcute/util-text': 1.2.0 - '@standard-schema/spec': 1.1.0 - esm-env: 1.2.2 - - '@atcute/mst@1.0.0': - dependencies: - '@atcute/cbor': 2.3.2 - '@atcute/cid': 2.4.1 - '@atcute/uint8array': 1.1.1 - - '@atcute/multibase@1.2.0': - dependencies: - '@atcute/uint8array': 1.1.1 - - '@atcute/oauth-crypto@0.1.0': - dependencies: - '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 - '@badrap/valita': 0.4.6 - nanoid: 5.1.7 - - '@atcute/oauth-keyset@0.1.0': - dependencies: - '@atcute/oauth-crypto': 0.1.0 - - '@atcute/oauth-node-client@1.1.0': - dependencies: - '@atcute/client': 4.2.1 - '@atcute/identity': 1.1.4 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicons': 1.2.9 - '@atcute/oauth-crypto': 0.1.0 - '@atcute/oauth-keyset': 0.1.0 - '@atcute/oauth-types': 0.1.1 - '@atcute/util-fetch': 1.0.5 - '@badrap/valita': 0.4.6 - nanoid: 5.1.7 - - '@atcute/oauth-types@0.1.1': - dependencies: - '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 - '@atcute/oauth-keyset': 0.1.0 - '@badrap/valita': 0.4.6 - - '@atcute/repo@0.1.3': - dependencies: - '@atcute/car': 5.1.1 - '@atcute/cbor': 2.3.2 - '@atcute/cid': 2.4.1 - '@atcute/crypto': 2.4.1 - '@atcute/lexicons': 1.2.9 - '@atcute/mst': 1.0.0 - '@atcute/uint8array': 1.1.1 - - '@atcute/tid@1.1.2': - dependencies: - '@atcute/time-ms': 1.3.2 - - '@atcute/time-ms@1.3.2': {} - - '@atcute/uint8array@1.1.1': {} - - '@atcute/util-fetch@1.0.5': - dependencies: - '@badrap/valita': 0.4.6 - - '@atcute/util-text@1.2.0': - dependencies: - unicode-segmenter: 0.14.5 - - '@atcute/varint@2.0.0': {} - - '@badrap/valita@0.4.6': {} - - '@cloudflare/kv-asset-handler@0.4.2': {} - - '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260317.1 - - '@cloudflare/workerd-darwin-64@1.20260317.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260317.1': - optional: true - - '@cloudflare/workerd-linux-64@1.20260317.1': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20260317.1': - optional: true - - '@cloudflare/workerd-windows-64@1.20260317.1': - optional: true - - '@cloudflare/workers-types@4.20260317.1': {} - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@emnapi/core@1.9.1': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/aix-ppc64@0.27.4': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.4': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.4': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.4': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.4': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.4': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.4': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.4': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.4': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.4': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.4': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.4': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.4': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.4': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.4': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.4': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.4': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.27.4': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.27.4': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.27.4': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.27.4': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.27.4': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.27.4': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.27.4': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.27.4': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - - '@esbuild/win32-x64@0.27.4': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': - dependencies: - eslint: 10.1.0(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/compat@2.0.3(eslint@10.1.0(jiti@2.6.1))': - dependencies: - '@eslint/core': 1.1.1 - optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) - - '@eslint/config-array@0.23.3': - dependencies: - '@eslint/object-schema': 3.0.3 - debug: 4.4.3 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.5.3': - dependencies: - '@eslint/core': 1.1.1 - - '@eslint/core@1.1.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': - optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) - - '@eslint/object-schema@3.0.3': {} - - '@eslint/plugin-kit@0.6.1': - dependencies: - '@eslint/core': 1.1.1 - levn: 0.4.1 - - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/utils@0.2.11': {} - - '@foxui/core@0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2)': - dependencies: - '@number-flow/svelte': 0.4.0(svelte@5.55.0) - bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - clsx: 2.1.1 - dompurify: 3.3.3 - mode-watcher: 1.1.0(svelte@5.55.0) - svelte: 5.55.0 - svelte-sonner: 1.1.0(svelte@5.55.0) - tailwind-merge: 3.5.0 - tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) - tailwindcss: 4.2.2 - web-haptics: 0.0.6(svelte@5.55.0) - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - react - - react-dom - - vue - - '@foxui/social@0.8.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2)': - dependencies: - '@atcute/bluesky': 3.3.0 - '@atcute/bluesky-richtext-segmenter': 3.0.0 - '@foxui/core': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) - '@foxui/text': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2) - '@foxui/time': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) - '@use-gesture/vanilla': 10.3.1 - bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - emoji-picker-element: 1.29.1 - hls.js: 1.6.15 - is-emoji-supported: 0.0.5 - plyr: 3.8.4 - svelte: 5.55.0 - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - '@tiptap/extension-code-block' - - '@tiptap/extensions' - - highlight.js - - react - - react-dom - - vue - - '@foxui/text@0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@foxui/core': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/extension-bubble-menu': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-code-block-lowlight': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(highlight.js@11.11.1)(lowlight@3.3.0) - '@tiptap/extension-document': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-dropcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-floating-menu': 3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-history': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-image': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-link': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-paragraph': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-placeholder': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-strike': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-text': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-typography': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-underline': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/markdown': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - '@tiptap/starter-kit': 3.21.0 - '@tiptap/suggestion': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - lowlight: 3.3.0 - state: link:@tiptap/pm/state - svelte: 5.55.0 - svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(svelte@5.55.0) - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - '@tiptap/extension-code-block' - - '@tiptap/extensions' - - highlight.js - - react - - react-dom - - vue - - '@foxui/time@0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2)': - dependencies: - '@foxui/core': 0.8.2(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2) - '@number-flow/svelte': 0.4.0(svelte@5.55.0) - bits-ui: 2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - svelte: 5.55.0 - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@internationalized/date' - - '@sveltejs/kit' - - react - - react-dom - - vue - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.9.1 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - - '@internationalized/date@3.10.1': - dependencies: - '@swc/helpers': 0.5.20 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@mary-ext/event-iterator@1.0.0': - dependencies: - yocto-queue: 1.2.2 - - '@mary-ext/simple-event-emitter@1.0.1': {} - - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': - dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@noble/secp256k1@3.0.0': {} - - '@number-flow/svelte@0.4.0(svelte@5.55.0)': - dependencies: - esm-env: 1.2.2 - number-flow: 0.6.0 - svelte: 5.55.0 - - '@optique/core@0.6.11': {} - - '@optique/run@0.6.11': - dependencies: - '@optique/core': 0.6.11 - - '@oxc-project/types@0.122.0': {} - - '@polka/url@1.0.0-next.29': {} - - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.6.5': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - - '@remirror/core-constants@3.0.0': {} - - '@rolldown/binding-android-arm64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': - dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.12': {} - - '@sindresorhus/is@7.2.0': {} - - '@speed-highlight/core@1.2.15': {} - - '@standard-schema/spec@1.1.0': {} - - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': - dependencies: - acorn: 8.16.0 - - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.78.0(@cloudflare/workers-types@4.20260317.1))': - dependencies: - '@cloudflare/workers-types': 4.20260317.1 - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - worktop: 0.8.0-next.18 - wrangler: 4.78.0(@cloudflare/workers-types@4.20260317.1) - - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - '@types/cookie': 0.6.0 - acorn: 8.16.0 - cookie: 0.6.0 - devalue: 5.6.4 - esm-env: 1.2.2 - kleur: 4.1.5 - magic-string: 0.30.21 - mrmime: 2.0.1 - set-cookie-parser: 3.1.0 - sirv: 3.0.2 - svelte: 5.55.0 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - optionalDependencies: - typescript: 6.0.2 - - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - deepmerge: 4.3.1 - magic-string: 0.30.21 - obug: 2.1.1 - svelte: 5.55.0 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - vitefu: 1.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - - '@swc/helpers@0.5.20': - dependencies: - tslib: 2.8.1 - - '@tailwindcss/forms@0.5.11(tailwindcss@4.2.2)': - dependencies: - mini-svg-data-uri: 1.4.4 - tailwindcss: 4.2.2 - - '@tailwindcss/node@4.2.2': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.2.2 - - '@tailwindcss/oxide-android-arm64@4.2.2': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.2.2': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.2.2': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - optional: true - - '@tailwindcss/oxide@4.2.2': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - - '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - - '@tiptap/core@3.21.0(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/pm': 3.21.0 - - '@tiptap/extension-blockquote@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-bold@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/extension-bullet-list@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-code-block-lowlight@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(highlight.js@11.11.1)(lowlight@3.3.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/extension-code-block': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - highlight.js: 11.11.1 - lowlight: 3.3.0 - - '@tiptap/extension-code-block@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/extension-code@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-document@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-dropcursor@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/extension-gapcursor@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-hard-break@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-heading@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-history@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-horizontal-rule@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/extension-image@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-italic@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-link@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - linkifyjs: 4.3.2 - - '@tiptap/extension-list-item@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-list-keymap@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/extension-ordered-list@3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-paragraph@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-placeholder@3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - - '@tiptap/extension-strike@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-text@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-typography@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extension-underline@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - - '@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/markdown@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - marked: 17.0.5 - - '@tiptap/pm@3.21.0': - dependencies: - prosemirror-changeset: 2.4.0 - prosemirror-collab: 1.3.1 - prosemirror-commands: 1.7.1 - prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.4.1 - prosemirror-history: 1.5.0 - prosemirror-inputrules: 1.5.1 - prosemirror-keymap: 1.2.3 - prosemirror-markdown: 1.13.4 - prosemirror-menu: 1.3.0 - prosemirror-model: 1.25.4 - prosemirror-schema-basic: 1.2.4 - prosemirror-schema-list: 1.5.1 - prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.5 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.7 - - '@tiptap/starter-kit@3.21.0': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/extension-blockquote': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-bold': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-bullet-list': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-code': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-code-block': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-document': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-dropcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-gapcursor': 3.21.0(@tiptap/extensions@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-hard-break': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-heading': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-horizontal-rule': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-italic': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-link': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-list': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-list-item': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-list-keymap': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-ordered-list': 3.21.0(@tiptap/extension-list@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)) - '@tiptap/extension-paragraph': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-strike': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-text': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extension-underline': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0)) - '@tiptap/extensions': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tiptap/suggestion@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)': - dependencies: - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/cookie@0.6.0': {} - - '@types/esrecurse@4.3.1': {} - - '@types/estree@1.0.8': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/json-schema@7.0.15': {} - - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@2.0.0': {} - - '@types/node@25.0.10': - dependencies: - undici-types: 7.16.0 - optional: true - - '@types/trusted-types@2.0.7': {} - - '@types/unist@3.0.3': {} - - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.57.2 - eslint: 10.1.0(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3 - eslint: 10.1.0(jiti@2.6.1) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.57.2(typescript@6.0.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@6.0.2) - '@typescript-eslint/types': 8.57.2 - debug: 4.4.3 - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.57.2': - dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/visitor-keys': 8.57.2 - - '@typescript-eslint/tsconfig-utils@8.57.2(typescript@6.0.2)': - dependencies: - typescript: 6.0.2 - - '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - debug: 4.4.3 - eslint: 10.1.0(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.57.2': {} - - '@typescript-eslint/typescript-estree@8.57.2(typescript@6.0.2)': - dependencies: - '@typescript-eslint/project-service': 8.57.2(typescript@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@6.0.2) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/visitor-keys': 8.57.2 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) - eslint: 10.1.0(jiti@2.6.1) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.57.2': - dependencies: - '@typescript-eslint/types': 8.57.2 - eslint-visitor-keys: 5.0.1 - - '@use-gesture/core@10.3.1': {} - - '@use-gesture/vanilla@10.3.1': - dependencies: - '@use-gesture/core': 10.3.1 - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - ajv@6.14.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - argparse@2.0.1: {} - - aria-query@5.3.1: {} - - axobject-query@4.1.0: {} - - balanced-match@4.0.4: {} - - bits-ui@2.16.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0): - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/dom': 1.7.6 - '@internationalized/date': 3.10.1 - esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - svelte: 5.55.0 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - tabbable: 6.4.0 - transitivePeerDependencies: - - '@sveltejs/kit' - - blake3-wasm@2.1.5: {} - - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - clsx@2.1.1: {} - - contrail@https://codeload.github.com/flo-bit/contrail/tar.gz/ee24d4faa5d5e1515fa71398bca035b191880dbf(@atcute/identity@1.1.4): - dependencies: - '@atcute/atproto': 3.1.10 - '@atcute/client': 4.2.1 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/jetstream': 1.1.2 - '@atcute/lexicons': 1.2.9 - hono: 4.12.9 - transitivePeerDependencies: - - '@atcute/identity' - - react - - cookie@0.6.0: {} - - cookie@1.1.1: {} - - core-js@3.49.0: {} - - crelt@1.0.6: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - cssesc@3.0.0: {} - - custom-event-polyfill@1.0.7: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - dequal@2.0.3: {} - - detect-libc@2.1.2: {} - - devalue@5.6.4: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - dompurify@3.3.3: - optionalDependencies: - '@types/trusted-types': 2.0.7 - - emoji-picker-element@1.29.1: {} - - enhanced-resolve@5.20.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.2 - - entities@4.5.0: {} - - error-stack-parser-es@1.0.5: {} - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - esbuild@0.27.4: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 - - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)): - dependencies: - eslint: 10.1.0(jiti@2.6.1) - - eslint-plugin-svelte@3.16.0(eslint@10.1.0(jiti@2.6.1))(svelte@5.55.0): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.1.0(jiti@2.6.1) - esutils: 2.0.3 - globals: 16.5.0 - known-css-properties: 0.37.0 - postcss: 8.5.8 - postcss-load-config: 3.1.4(postcss@8.5.8) - postcss-safe-parser: 7.0.1(postcss@8.5.8) - semver: 7.7.4 - svelte-eslint-parser: 1.6.0(svelte@5.55.0) - optionalDependencies: - svelte: 5.55.0 - transitivePeerDependencies: - - ts-node - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.1.0(jiti@2.6.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.6.1 - transitivePeerDependencies: - - supports-color - - esm-env@1.2.2: {} - - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrap@2.2.4: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.57.2 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - event-target-polyfill@0.0.4: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.13.7: - dependencies: - resolve-pkg-maps: 1.0.0 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@16.5.0: {} - - globals@17.4.0: {} - - graceful-fs@4.2.11: {} - - highlight.js@11.11.1: {} - - hls.js@1.6.15: {} - - hono@4.12.9: {} - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - imurmurhash@0.1.4: {} - - inline-style-parser@0.2.7: {} - - is-emoji-supported@0.0.5: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - isexe@2.0.0: {} - - jiti@2.6.1: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kleur@4.1.5: {} - - known-css-properties@0.37.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lilconfig@2.1.0: {} - - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - linkifyjs@4.3.2: {} - - loadjs@4.3.0: {} - - locate-character@3.0.0: {} - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lowlight@3.3.0: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - highlight.js: 11.11.1 - - lz-string@1.5.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - markdown-it@14.1.1: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - marked@17.0.5: {} - - mdurl@2.0.0: {} - - mini-svg-data-uri@1.4.4: {} - - miniflare@4.20260317.3: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.24.4 - workerd: 1.20260317.1 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.5 - - mode-watcher@1.1.0(svelte@5.55.0): - dependencies: - runed: 0.25.0(svelte@5.55.0) - svelte: 5.55.0 - svelte-toolbelt: 0.7.1(svelte@5.55.0) - - mri@1.2.0: {} - - mrmime@2.0.1: {} - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - nanoid@5.1.7: {} - - natural-compare@1.4.0: {} - - number-flow@0.6.0: - dependencies: - esm-env: 1.2.2 - - obug@2.1.1: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - orderedmap@2.1.1: {} - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - partysocket@1.1.16: - dependencies: - event-target-polyfill: 0.0.4 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-to-regexp@6.3.0: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - plyr@3.8.4: - dependencies: - core-js: 3.49.0 - custom-event-polyfill: 1.0.7 - loadjs: 4.3.0 - rangetouch: 2.0.1 - url-polyfill: 1.1.14 - - postcss-load-config@3.1.4(postcss@8.5.8): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.3 - optionalDependencies: - postcss: 8.5.8 - - postcss-safe-parser@7.0.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-scss@4.0.9(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.0): - dependencies: - prettier: 3.8.1 - svelte: 5.55.0 - - prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.0))(prettier@3.8.1): - dependencies: - prettier: 3.8.1 - optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.0) - - prettier@3.8.1: {} - - prosemirror-changeset@2.4.0: - dependencies: - prosemirror-transform: 1.11.0 - - prosemirror-collab@1.3.1: - dependencies: - prosemirror-state: 1.4.4 - - prosemirror-commands@1.7.1: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - prosemirror-dropcursor@1.8.2: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.7 - - prosemirror-gapcursor@1.4.1: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.7 - - prosemirror-history@1.5.0: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.7 - rope-sequence: 1.3.4 - - prosemirror-inputrules@1.5.1: - dependencies: - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - prosemirror-keymap@1.2.3: - dependencies: - prosemirror-state: 1.4.4 - w3c-keyname: 2.2.8 - - prosemirror-markdown@1.13.4: - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.1 - prosemirror-model: 1.25.4 - - prosemirror-menu@1.3.0: - dependencies: - crelt: 1.0.6 - prosemirror-commands: 1.7.1 - prosemirror-history: 1.5.0 - prosemirror-state: 1.4.4 - - prosemirror-model@1.25.4: - dependencies: - orderedmap: 2.1.1 - - prosemirror-schema-basic@1.2.4: - dependencies: - prosemirror-model: 1.25.4 - - prosemirror-schema-list@1.5.1: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - prosemirror-state@1.4.4: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.7 - - prosemirror-tables@1.8.5: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - prosemirror-view: 1.41.7 - - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): - dependencies: - '@remirror/core-constants': 3.0.0 - escape-string-regexp: 4.0.0 - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-view: 1.41.7 - - prosemirror-transform@1.11.0: - dependencies: - prosemirror-model: 1.25.4 - - prosemirror-view@1.41.7: - dependencies: - prosemirror-model: 1.25.4 - prosemirror-state: 1.4.4 - prosemirror-transform: 1.11.0 - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - - rangetouch@2.0.1: {} - - readdirp@4.1.2: {} - - regexparam@3.0.0: {} - - resolve-pkg-maps@1.0.0: {} - - rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): - dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - rope-sequence@1.3.4: {} - - runed@0.23.4(svelte@5.55.0): - dependencies: - esm-env: 1.2.2 - svelte: 5.55.0 - - runed@0.25.0(svelte@5.55.0): - dependencies: - esm-env: 1.2.2 - svelte: 5.55.0 - - runed@0.28.0(svelte@5.55.0): - dependencies: - esm-env: 1.2.2 - svelte: 5.55.0 - - runed@0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0): - dependencies: - dequal: 2.0.3 - esm-env: 1.2.2 - lz-string: 1.5.0 - svelte: 5.55.0 - optionalDependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - - sade@1.8.1: - dependencies: - mri: 1.2.0 - - semver@7.7.4: {} - - set-cookie-parser@3.1.0: {} - - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - sirv@3.0.2: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - - source-map-js@1.2.1: {} - - style-to-object@1.0.14: - dependencies: - inline-style-parser: 0.2.7 - - supports-color@10.2.2: {} - - svelte-check@4.4.5(picomatch@4.0.4)(svelte@5.55.0)(typescript@6.0.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.4) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.55.0 - typescript: 6.0.2 - transitivePeerDependencies: - - picomatch - - svelte-eslint-parser@1.6.0(svelte@5.55.0): - dependencies: - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - postcss: 8.5.8 - postcss-scss: 4.0.9(postcss@8.5.8) - postcss-selector-parser: 7.1.1 - semver: 7.7.4 - optionalDependencies: - svelte: 5.55.0 - - svelte-sonner@1.1.0(svelte@5.55.0): - dependencies: - runed: 0.28.0(svelte@5.55.0) - svelte: 5.55.0 - - svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/extension-bubble-menu@3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/extension-floating-menu@3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0)(svelte@5.55.0): - dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.21.0(@tiptap/pm@3.21.0) - '@tiptap/extension-bubble-menu': 3.21.0(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/extension-floating-menu': 3.21.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.21.0(@tiptap/pm@3.21.0))(@tiptap/pm@3.21.0) - '@tiptap/pm': 3.21.0 - svelte: 5.55.0 - - svelte-toolbelt@0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0): - dependencies: - clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0) - style-to-object: 1.0.14 - svelte: 5.55.0 - transitivePeerDependencies: - - '@sveltejs/kit' - - svelte-toolbelt@0.7.1(svelte@5.55.0): - dependencies: - clsx: 2.1.1 - runed: 0.23.4(svelte@5.55.0) - style-to-object: 1.0.14 - svelte: 5.55.0 - - svelte@5.55.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 - '@types/trusted-types': 2.0.7 - acorn: 8.16.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.6.4 - esm-env: 1.2.2 - esrap: 2.2.4 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - - tabbable@6.4.0: {} - - tailwind-merge@3.5.0: {} - - tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2): - dependencies: - tailwindcss: 4.2.2 - optionalDependencies: - tailwind-merge: 3.5.0 - - tailwindcss@4.2.2: {} - - tapable@2.3.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - totalist@3.0.1: {} - - ts-api-utils@2.5.0(typescript@6.0.2): - dependencies: - typescript: 6.0.2 - - tslib@2.8.1: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.4 - get-tsconfig: 4.13.7 - optionalDependencies: - fsevents: 2.3.3 - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@4.41.0: {} - - typescript-eslint@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2): - dependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/typescript-estree': 8.57.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) - eslint: 10.1.0(jiti@2.6.1) - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - - typescript@6.0.2: {} - - uc.micro@2.1.0: {} - - undici-types@7.16.0: - optional: true - - undici@7.24.4: {} - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - - unicode-segmenter@0.14.5: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - url-polyfill@1.1.14: {} - - util-deprecate@1.0.2: {} - - valibot@1.3.1(typescript@6.0.2): - optionalDependencies: - typescript: 6.0.2 - - vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.8 - rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.0.10 - esbuild: 0.27.4 - fsevents: 2.3.3 - jiti: 2.6.1 - tsx: 4.21.0 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - vitefu@1.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): - optionalDependencies: - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - - w3c-keyname@2.2.8: {} - - web-haptics@0.0.6(svelte@5.55.0): - optionalDependencies: - svelte: 5.55.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - workerd@1.20260317.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260317.1 - '@cloudflare/workerd-darwin-arm64': 1.20260317.1 - '@cloudflare/workerd-linux-64': 1.20260317.1 - '@cloudflare/workerd-linux-arm64': 1.20260317.1 - '@cloudflare/workerd-windows-64': 1.20260317.1 - - worktop@0.8.0-next.18: - dependencies: - mrmime: 2.0.1 - regexparam: 3.0.0 - - wrangler@4.78.0(@cloudflare/workers-types@4.20260317.1): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) - blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260317.3 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260317.1 - optionalDependencies: - '@cloudflare/workers-types': 4.20260317.1 - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - ws@8.18.0: {} - - yaml@1.10.3: {} - - yocto-queue@0.1.0: {} - - yocto-queue@1.2.2: {} - - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.15 - cookie: 1.1.1 - youch-core: 0.3.3 - - zimmerframe@1.1.4: {} diff --git a/package.json b/package.json index 2b915bd..1abdd07 100644 --- a/package.json +++ b/package.json @@ -1,108 +1,20 @@ { - "name": "@atmo-dev/contrail", - "version": "0.1.1", - "description": "Index AT Protocol records with typed XRPC endpoints. Cloudflare Workers + D1, SvelteKit, Node.js.", + "name": "contrail-monorepo", + "private": true, + "version": "0.0.0", "type": "module", - "sideEffects": false, - "files": [ - "dist", - "lexicon-templates/**/*.json" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./server": { - "types": "./dist/server.d.ts", - "import": "./dist/server.js" - }, - "./sqlite": { - "types": "./dist/adapters/sqlite.d.ts", - "import": "./dist/adapters/sqlite.js" - }, - "./postgres": { - "types": "./dist/adapters/postgres.d.ts", - "import": "./dist/adapters/postgres.js" - }, - "./generate": { - "types": "./dist/generate.d.ts", - "import": "./dist/generate.js" - }, - "./publish": { - "types": "./dist/publish.d.ts", - "import": "./dist/publish.js" - }, - "./sync": { - "types": "./dist/sync/index.d.ts", - "import": "./dist/sync/index.js" - }, - "./sync/cache-idb": { - "types": "./dist/sync/cache-idb.d.ts", - "import": "./dist/sync/cache-idb.js" - }, - "./lexicon-templates/*.json": "./lexicon-templates/*.json" - }, - "repository": { - "type": "git", - "url": "https://github.com/flo-bit/contrail.git" - }, - "keywords": [ - "atproto", - "bluesky", - "cloudflare", - "d1", - "indexer", - "xrpc" - ], "scripts": { - "build": "tsup", - "release": "changeset publish", - "dev": "wrangler dev --test-scheduled", - "dev:auto": "tsx scripts/dev.ts", - "deploy": "wrangler deploy", - "clean": "tsx scripts/clean.ts", - "generate": "tsx app/generate.ts", - "generate:pull": "tsx app/generate.ts && lex-cli pull && tsx app/generate.ts && lex-cli pull && lex-cli generate", - "publish-lexicons": "tsx scripts/publish-lexicons.ts", - "typecheck": "tsc --noEmit", - "ingest": "curl -s http://localhost:8787/__scheduled?cron=*/1+*+*+*+*", - "sync": "tsx app/sync.ts", - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "@atcute/atproto": "^3.1.10", - "@atcute/cid": "^2.4.1", - "@atcute/client": "^4.2.1", - "@atcute/identity": "^1.1.4", - "@atcute/identity-resolver": "^1.2.2", - "@atcute/jetstream": "^1.0.2", - "@atcute/lexicons": "^1.2.9", - "@atcute/xrpc-server": "^0.1.12", - "hono": "^4.12.8" + "build": "turbo run build", + "test": "turbo run test", + "typecheck": "turbo run typecheck", + "dev": "turbo run dev", + "clean": "turbo run clean", + "release": "turbo run build && changeset publish" }, "devDependencies": { - "@atcute/lex-cli": "^2.5.3", - "@atcute/lexicon-doc": "^2.1.2", "@changesets/cli": "^2.29.4", - "@cloudflare/workers-types": "^4.20250124.0", - "@types/node": "^25.5.0", - "@types/pg": "^8.20.0", - "pg": "^8.20.0", - "tsup": "^8.5.0", - "tsx": "^4.21.0", - "typescript": "^5.7.3", - "vitest": "^4.1.0", - "wrangler": "^4.63.0" - }, - "peerDependencies": { - "pg": "^8.0.0" - }, - "peerDependenciesMeta": { - "pg": { - "optional": true - } + "turbo": "^2.3.3", + "typescript": "^5.7.3" }, - "license": "MIT" + "packageManager": "pnpm@10.30.1" } diff --git a/CHANGELOG.md b/packages/contrail/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/contrail/CHANGELOG.md diff --git a/PERMISSIONED_DATA.md b/packages/contrail/PERMISSIONED_DATA.md similarity index 100% rename from PERMISSIONED_DATA.md rename to packages/contrail/PERMISSIONED_DATA.md diff --git a/packages/contrail/README.md b/packages/contrail/README.md new file mode 100644 index 0000000..4e95bd6 --- /dev/null +++ b/packages/contrail/README.md @@ -0,0 +1,444 @@ +# Contrail + +> [!WARNING] +> Work in progress! Pre-alpha, expect breaking changes. + +A library for indexing AT Protocol records. Define collections — get automatic Jetstream ingestion, PDS backfill, user discovery, typed XRPC endpoints, and (opt-in) permissioned spaces for private records. Works with Cloudflare Workers + D1, SvelteKit, Node.js, or any JavaScript runtime. + +## Install + +```bash +npm install @atmo-dev/contrail +``` + +## Usage + +```ts +import { Contrail } from "@atmo-dev/contrail"; + +const contrail = new Contrail({ + namespace: "com.example", + db, // any Database-compatible instance (D1, SQLite, etc.) + collections: { + event: { // short name → URL path + table suffix + collection: "community.lexicon.calendar.event", // full NSID of the record type + queryable: { + mode: {}, // string → equality filter (?mode=online) + name: {}, // string → equality filter (?name=...) + startsAt: { type: "range" }, // range → min/max filters (?startsAtMin=...&startsAtMax=...) + endsAt: { type: "range" }, + }, + searchable: ["name", "description"], + relations: { + rsvps: { + collection: "rsvp", // short name of the child collection + groupBy: "status", + count: true, + groups: { + interested: "community.lexicon.calendar.rsvp#interested", + going: "community.lexicon.calendar.rsvp#going", + notgoing: "community.lexicon.calendar.rsvp#notgoing", + }, + }, + }, + }, + rsvp: { + collection: "community.lexicon.calendar.rsvp", + queryable: { + status: {}, + "subject.uri": {}, + }, + references: { + event: { + collection: "event", // short name of the referenced collection + field: "subject.uri", + }, + }, + }, + }, +}); + +await contrail.init(); +``` + +### Query records + +```ts +const { records, cursor } = await contrail.query( + "event", // short name you declared in `collections` + { + filters: { mode: "in-person" }, + sort: { countType: "rsvp", direction: "desc" }, + limit: 20, + } +); +``` + +### Ingest from Jetstream + +```ts +// Run one ingestion cycle (catches up to present, then stops) +await contrail.ingest(); +``` + +### Persistent ingestion + +```ts +// Long-lived Jetstream connection with automatic batching and reconnection +const controller = new AbortController(); +await contrail.runPersistent({ + batchSize: 50, // flush every N events (default: 50) + flushIntervalMs: 5000, // or every N ms (default: 5000) + signal: controller.signal, +}); +``` + +Call `controller.abort()` for graceful shutdown — the current batch is flushed and the cursor is saved. + +### Discover users and backfill + +```ts +// Find users from relays +await contrail.discover(); + +// Backfill their records from PDS +await contrail.backfill({ concurrency: 100 }); + +// Or both in one call +await contrail.sync({ concurrency: 100 }); +``` + +### Notify of immediate updates + +```ts +// After writing to a user's PDS, tell Contrail to fetch it now +await contrail.notify("at://did:plc:abc/community.lexicon.calendar.rsvp/123"); + +// Batch up to 25 URIs +await contrail.notify([uri1, uri2, uri3]); +``` + +### HTTP handler (XRPC endpoints) + +Mount the full XRPC API in any framework: + +```ts +import { createHandler } from "@atmo-dev/contrail/server"; + +const handle = createHandler(contrail); +// handle: (Request, db?) => Promise +``` + +**SvelteKit:** + +```ts +// src/routes/xrpc/[...path]/+server.ts +export const GET = ({ request }) => handle(request); +export const POST = ({ request }) => handle(request); +``` + +**Cloudflare Worker:** + +```ts +export default { + async fetch(request, env) { + return handle(request, env.DB); + }, +}; +``` + +### SQLite adapter (Node.js / local dev) + +```ts +import { createSqliteDatabase } from "@atmo-dev/contrail/sqlite"; + +const db = createSqliteDatabase("data.db"); +const contrail = new Contrail({ ...config, db }); +``` + +> **Note:** The SQLite adapter uses Node's built-in `node:sqlite` (Node 22+). Full-text search (`searchable`) is not supported with this adapter because `node:sqlite` doesn't include the FTS5 extension. Search works on Cloudflare D1 and PostgreSQL. + +### PostgreSQL adapter (Node.js / server) + +```ts +import { createPostgresDatabase } from "@atmo-dev/contrail/postgres"; +import pg from "pg"; + +const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); +const db = createPostgresDatabase(pool); +const contrail = new Contrail({ ...config, db }); +``` + +PostgreSQL uses JSONB for record storage, tsvector generated columns for full-text search (instead of FTS5), and `BIGINT` for timestamp columns. + +## Examples + +### PostgreSQL (Node.js) + +See [`examples/postgres/`](examples/postgres/) for a complete example with Docker Compose, persistent Jetstream ingestion, user discovery/backfill, and an HTTP API server. + +### Cloudflare Workers + +This repo includes a working example that indexes AT Protocol calendar events and RSVPs on Cloudflare Workers + D1. + +### Setup + +```bash +pnpm install +pnpm generate:pull # pull lexicons from network, auto-detect fields, generate types +``` + +### Dev + +```bash +pnpm sync # discover users and backfill records from PDS +pnpm dev:auto # start wrangler dev with auto-ingestion +``` + +### Production + +```bash +npx wrangler d1 create contrail +# Add database_id to wrangler.jsonc +pnpm deploy +pnpm sync # discover + backfill against prod D1 +``` + +Ingestion runs automatically via cron (`*/1 * * * *`). Schema is auto-initialized. + +## Config + +### Collection options + +| Option | Default | Description | +|--------|---------|-------------| +| `queryable` | `{}` | Fields exposed as query filters. `{}` = string equality, `{ type: "range" }` = min/max | +| `discover` | `true` | Find users via relays. `false` = only track known DIDs | +| `relations` | `{}` | Many-to-one relationships with materialized counts | +| `relations.*.field` | `"subject.uri"` | Field in the related record to match against | +| `relations.*.match` | `"uri"` | Match against parent's `"uri"` or `"did"` | +| `relations.*.groupBy` | — | Split counts by this field's value | +| `relations.*.groups` | — | Group value mappings (e.g. `{ going: "collection#going" }`) | +| `relations.*.count` | `true` | Enable materialized count columns on the parent | +| `references` | `{}` | Forward references to other collections for hydration | +| `references.*.collection` | — | Short name of the target collection (key in `collections`) | +| `references.*.field` | — | Field containing the target record's AT URI | +| `queries` | `{}` | Custom query handlers (raw Response) | +| `pipelineQueries` | `{}` | Custom query handlers that go through the standard filter/sort/hydration pipeline | +| `searchable` | disabled | Full-text search fields. SQLite uses FTS5 virtual tables; PostgreSQL uses tsvector generated columns with GIN indexes. Provide `string[]` to enable, omit to disable | +| `collection` | — | Full NSID of the record type this short-name entry indexes (required) | +| `methods` | `["listRecords", "getRecord"]` | XRPC methods to emit for this collection | +| `allowInSpaces` | `true` | When spaces are enabled, emit a parallel `spaces_records_` table | + +### Top-level options + +| Option | Default | Description | +|--------|---------|-------------| +| `namespace` | — | Your reverse-domain namespace (e.g. `"com.example"`) | +| `collections` | — | Collection configurations | +| `profiles` | `["app.bsky.actor.profile"]` | Profile collection NSIDs | +| `relays` | Bluesky relays | Relay URLs for user discovery | +| `jetstreams` | Bluesky Jetstream | Jetstream URLs for real-time ingestion | +| `feeds` | — | Personalized feed configurations | +| `notify` | off | Expose `notifyOfUpdate`. `true` = open, string = `Authorization: Bearer ` required | +| `spaces` | — | Permissioned-spaces configuration. See [PERMISSIONED_DATA.md](./PERMISSIONED_DATA.md) | +| `logger` | `console` | Logger instance (`{ log, warn, error }`) | + +### Profiles + +`profiles` is a top-level config array of collection NSIDs that contain profile records (rkey `self`). Defaults to `["app.bsky.actor.profile"]`. These are auto-added to `collections` with `{ discover: false }`. Use `?profiles=true` on any endpoint to include a `profiles` map in the response, keyed by DID, with handle and profile record data. + +## XRPC API + +When using `createHandler`, all endpoints live under the deployment's own namespace at `/xrpc/{namespace}.{...}`: + +| Endpoint | Description | +|----------|-------------| +| `{namespace}.{short}.listRecords` | List/filter records in a collection (keyed by its short name) | +| `{namespace}.{short}.getRecord` | Get single record by URI | +| `{namespace}.getProfile` | Get a user's profile by DID or handle | +| `{namespace}.notifyOfUpdate` | Notify of a record change for immediate indexing | +| `{namespace}.getCursor` | Current cursor position | +| `{namespace}.getOverview` | All collections summary | +| `{namespace}.permissionSet` | OAuth permission-set bundling every method above (auto-generated) | +| `{namespace}.space.*` | Spaces admin, invite, member, record XRPCs (when `spaces` is enabled) | + +### Query parameters + +**Filtering:** + +| Param | Example | Description | +|-------|---------|-------------| +| `actor` | `?actor=did:plc:...` or `?actor=alice.bsky.social` | Filter by DID or handle (triggers on-demand backfill) | +| `profiles` | `?profiles=true` | Include profile + identity info keyed by DID | +| `search` | `?search=meetup` | Full-text search across searchable fields (FTS5, ranked) | +| `{field}` | `?status=going` | Equality filter on queryable string field | +| `{field}Min` | `?startsAtMin=2026-03-16` | Range minimum (datetime/integer fields) | +| `{field}Max` | `?endsAtMax=2026-04-01` | Range maximum (datetime/integer fields) | +| `{rel}CountMin` | `?rsvpsCountMin=10` | Minimum total relation count | +| `{rel}{Group}CountMin` | `?rsvpsGoingCountMin=10` | Minimum relation count for a specific groupBy value | +| `hydrate{Rel}` | `?hydrateRsvps=10` | Embed latest N related records (per group if grouped) | +| `hydrate{Ref}` | `?hydrateEvent=true` | Embed the referenced record | +| `sort` | `?sort=startsAt` | Sort by a queryable field or count (see below) | +| `order` | `?order=asc` | Sort direction: `asc` or `desc` (default depends on field type) | +| `limit` | `?limit=25` | Page size (1-200, default 50) | +| `cursor` | `?cursor=...` | Pagination cursor | + +**Sorting** — `sort` accepts any queryable field param name or a count field: + +``` +?sort=startsAt # by date (default: desc for range fields) +?sort=name&order=asc # by name ascending +?sort=rsvpsCount # by total RSVP count (default: desc) +?sort=rsvpsGoingCount&order=asc # by going count ascending +``` + +**Search** uses SQLite FTS5 or PostgreSQL tsvector for ranked full-text search. To enable, set `searchable: ["field1", "field2"]` on a collection. Supports FTS5 syntax including prefix (`meetup*`), phrases (`"rust meetup"`), and boolean (`rust OR typescript`). Combinable with all other filters. + +``` +?search=meetup # basic search +?search=meetup&mode=online # search + filter +?search=rust*&sort=startsAt&order=asc # search + sort override +``` + +**Hydration** embeds related or referenced records inline: + +``` +?hydrateRsvps=5 # latest 5 RSVPs per group (going, interested, etc.) +?hydrateEvent=true # embed the referenced event record +?hydrateRsvps=5&hydrateEvent=true # combine both +``` + +### Examples (events) + +``` +# Upcoming events with 10+ going RSVPs, with RSVP records and profiles +/xrpc/com.example.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10&hydrateRsvps=5&profiles=true + +# Events for a specific user (by handle) +/xrpc/com.example.event.listRecords?actor=alice.bsky.social&profiles=true + +# Single event with counts, RSVPs, and profiles +/xrpc/com.example.event.getRecord?uri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateRsvps=10&profiles=true + +# Search for events by name/description +/xrpc/com.example.event.listRecords?search=meetup&profiles=true + +# RSVPs for a specific event, with the referenced event embedded +/xrpc/com.example.rsvp.listRecords?subjectUri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateEvent=true&profiles=true +``` + +## Notify of Updates + +By default, Contrail ingests from Jetstream every minute (in the Worker example). If your app writes to a user's PDS and needs the change reflected immediately, use `contrail.notify()` or call the XRPC endpoint: + +```ts +// Programmatic +await contrail.notify(uri); + +// Or via HTTP +await fetch("https://your-contrail.workers.dev/xrpc/com.example.notifyOfUpdate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ uri }), +}); +``` + +Contrail fetches the record from the user's PDS and figures out what to do: + +| PDS returns | Already indexed? | Action | +|---|---|---| +| Record (new CID) | No | **Create** — indexes it, recounts relations | +| Record (new CID) | Yes | **Update** — upserts the record, recounts relations | +| Record (same CID) | Yes | **Skip** — nothing changed | +| 404 | Yes | **Delete** — removes it, recounts relations | +| 404 | No | **No-op** | + +When Jetstream later delivers the same event, the duplicate is detected by CID and skipped. + +## Permissioned Data + +Contrail ships an opt-in permissioned-spaces subsystem: an auth-gated store for records that can't live on public PDSes — private events, invite-only groups, members-only chat. Set `config.spaces` and contrail exposes the space XRPCs at `{namespace}.space.*` alongside your public indexer: + +```ts +const contrail = new Contrail({ + namespace: "com.example", + collections: { /* ... */ }, + spaces: { + type: "com.example.event.space", // NSID classifying the kind of space + serviceDid: "did:web:example.com", // your deployment's DID + // `resolver` is optional — defaults to a composite did:plc + did:web resolver. + }, +}); +``` + +Each collection you declare also gets a parallel `spaces_records_` table (opt out per-collection via `allowInSpaces: false`). Auth uses atproto service-auth JWTs via `@atcute/xrpc-server`. Access is a simple `read` / `write` permission per member — the space owner is implicit write. Invites are first-class (generated token, hashed-at-rest, expiry + max-uses + revocation). + +**Unified `listRecords`.** The per-collection `listRecords` endpoint accepts three call shapes: + +| Call | Returns | +| --- | --- | +| No auth, no `spaceUri` | Public records only | +| `?spaceUri=…` + service-auth JWT | Records from that one space (ACL-gated) | +| Service-auth JWT, no `spaceUri` | Public records **unioned** with records from every space the caller is a member of | + +The union path runs the public and per-space queries in parallel and merges with a shared keyset cursor, so filters, sorts (`time`, record-field, count), hydration, and references all work across sources. Records from a space carry a `space: ` field in the response. + +Full design, migration story, and known limits: [PERMISSIONED_DATA.md](./PERMISSIONED_DATA.md). + +## Typesafe Client Usage + +You can get fully typed XRPC queries for any Contrail instance using [`@atcute/lex-cli`](https://github.com/mary-ext/atcute). The lexicon files are committed to the repo, so you can pull them directly via the git source. + +### Setup + +```bash +npm install @atcute/client @atcute/lexicons @atcute/lex-cli +``` + +Create a `lex.config.js` pointing at the Contrail instance's repo: + +```js +import { defineLexiconConfig } from "@atcute/lex-cli"; + +export default defineLexiconConfig({ + outdir: "src/lexicon-types/", + imports: ["@atcute/atproto"], + files: ["lexicons/**/*.json"], + pull: { + outdir: "lexicons/", + sources: [ + { + type: "git", + remote: "https://github.com/USER/REPO.git", // the Contrail instance repo + pattern: ["lexicons-generated/**/*.json", "lexicons-pulled/**/*.json", "lexicons/**/*.json"], + }, + ], + }, +}); +``` + +Then pull and generate: + +```bash +npx lex-cli pull && npx lex-cli generate +``` + +### Usage + +Import the generated types (side-effect import registers them with `@atcute/client`), then query with full type safety: + +```ts +import "./lexicon-types/index.js"; // registers ambient types +import { Client } from "@atcute/client"; + +const rpc = new Client({ handler: simpleFetchHandler({ service: /* your contrail url */ }) }); + +const response = await rpc.get("com.example.rsvp.listRecords", { + params: { status: "going", limit: 10 }, // typed params +}); + +if (response.ok) { + console.log(response.data.records); // typed +} +``` diff --git a/packages/contrail/package.json b/packages/contrail/package.json new file mode 100644 index 0000000..5fec7a6 --- /dev/null +++ b/packages/contrail/package.json @@ -0,0 +1,77 @@ +{ + "name": "@atmo-dev/contrail", + "version": "0.1.1", + "description": "Index AT Protocol records with typed XRPC endpoints. Cloudflare Workers + D1, SvelteKit, Node.js.", + "type": "module", + "sideEffects": false, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server.d.ts", + "import": "./dist/server.js" + }, + "./sqlite": { + "types": "./dist/adapters/sqlite.d.ts", + "import": "./dist/adapters/sqlite.js" + }, + "./postgres": { + "types": "./dist/adapters/postgres.d.ts", + "import": "./dist/adapters/postgres.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/flo-bit/contrail.git", + "directory": "packages/contrail" + }, + "keywords": [ + "atproto", + "bluesky", + "cloudflare", + "d1", + "indexer", + "xrpc" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@atcute/atproto": "^3.1.10", + "@atcute/cid": "^2.4.1", + "@atcute/client": "^4.2.1", + "@atcute/identity": "^1.1.4", + "@atcute/identity-resolver": "^1.2.2", + "@atcute/jetstream": "^1.0.2", + "@atcute/lexicons": "^1.2.9", + "@atcute/xrpc-server": "^0.1.12", + "hono": "^4.12.8" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250124.0", + "@types/node": "^25.5.0", + "@types/pg": "^8.20.0", + "pg": "^8.20.0", + "tsup": "^8.5.0", + "typescript": "^5.7.3", + "vitest": "^4.1.0" + }, + "peerDependencies": { + "pg": "^8.0.0" + }, + "peerDependenciesMeta": { + "pg": { + "optional": true + } + }, + "license": "MIT" +} diff --git a/src/adapters/postgres.ts b/packages/contrail/src/adapters/postgres.ts similarity index 100% rename from src/adapters/postgres.ts rename to packages/contrail/src/adapters/postgres.ts diff --git a/src/adapters/sqlite.ts b/packages/contrail/src/adapters/sqlite.ts similarity index 100% rename from src/adapters/sqlite.ts rename to packages/contrail/src/adapters/sqlite.ts diff --git a/src/contrail.ts b/packages/contrail/src/contrail.ts similarity index 100% rename from src/contrail.ts rename to packages/contrail/src/contrail.ts diff --git a/src/core/backfill.ts b/packages/contrail/src/core/backfill.ts similarity index 100% rename from src/core/backfill.ts rename to packages/contrail/src/core/backfill.ts diff --git a/src/core/client.ts b/packages/contrail/src/core/client.ts similarity index 100% rename from src/core/client.ts rename to packages/contrail/src/core/client.ts diff --git a/src/core/community/acl.ts b/packages/contrail/src/core/community/acl.ts similarity index 100% rename from src/core/community/acl.ts rename to packages/contrail/src/core/community/acl.ts diff --git a/src/core/community/adapter.ts b/packages/contrail/src/core/community/adapter.ts similarity index 100% rename from src/core/community/adapter.ts rename to packages/contrail/src/core/community/adapter.ts diff --git a/src/core/community/credentials.ts b/packages/contrail/src/core/community/credentials.ts similarity index 100% rename from src/core/community/credentials.ts rename to packages/contrail/src/core/community/credentials.ts diff --git a/src/core/community/index.ts b/packages/contrail/src/core/community/index.ts similarity index 100% rename from src/core/community/index.ts rename to packages/contrail/src/core/community/index.ts diff --git a/src/core/community/pds.ts b/packages/contrail/src/core/community/pds.ts similarity index 100% rename from src/core/community/pds.ts rename to packages/contrail/src/core/community/pds.ts diff --git a/src/core/community/plc.ts b/packages/contrail/src/core/community/plc.ts similarity index 100% rename from src/core/community/plc.ts rename to packages/contrail/src/core/community/plc.ts diff --git a/src/core/community/reconcile.ts b/packages/contrail/src/core/community/reconcile.ts similarity index 100% rename from src/core/community/reconcile.ts rename to packages/contrail/src/core/community/reconcile.ts diff --git a/src/core/community/router.ts b/packages/contrail/src/core/community/router.ts similarity index 100% rename from src/core/community/router.ts rename to packages/contrail/src/core/community/router.ts diff --git a/src/core/community/schema.ts b/packages/contrail/src/core/community/schema.ts similarity index 100% rename from src/core/community/schema.ts rename to packages/contrail/src/core/community/schema.ts diff --git a/src/core/community/types.ts b/packages/contrail/src/core/community/types.ts similarity index 100% rename from src/core/community/types.ts rename to packages/contrail/src/core/community/types.ts diff --git a/src/core/db/index.ts b/packages/contrail/src/core/db/index.ts similarity index 100% rename from src/core/db/index.ts rename to packages/contrail/src/core/db/index.ts diff --git a/src/core/db/records.ts b/packages/contrail/src/core/db/records.ts similarity index 100% rename from src/core/db/records.ts rename to packages/contrail/src/core/db/records.ts diff --git a/src/core/db/schema.ts b/packages/contrail/src/core/db/schema.ts similarity index 100% rename from src/core/db/schema.ts rename to packages/contrail/src/core/db/schema.ts diff --git a/src/core/dialect.ts b/packages/contrail/src/core/dialect.ts similarity index 100% rename from src/core/dialect.ts rename to packages/contrail/src/core/dialect.ts diff --git a/src/core/identity.ts b/packages/contrail/src/core/identity.ts similarity index 100% rename from src/core/identity.ts rename to packages/contrail/src/core/identity.ts diff --git a/src/core/invite/index.ts b/packages/contrail/src/core/invite/index.ts similarity index 100% rename from src/core/invite/index.ts rename to packages/contrail/src/core/invite/index.ts diff --git a/src/core/invite/router.ts b/packages/contrail/src/core/invite/router.ts similarity index 100% rename from src/core/invite/router.ts rename to packages/contrail/src/core/invite/router.ts diff --git a/src/core/invite/token.ts b/packages/contrail/src/core/invite/token.ts similarity index 100% rename from src/core/invite/token.ts rename to packages/contrail/src/core/invite/token.ts diff --git a/src/core/jetstream.ts b/packages/contrail/src/core/jetstream.ts similarity index 100% rename from src/core/jetstream.ts rename to packages/contrail/src/core/jetstream.ts diff --git a/src/core/persistent.ts b/packages/contrail/src/core/persistent.ts similarity index 100% rename from src/core/persistent.ts rename to packages/contrail/src/core/persistent.ts diff --git a/src/core/realtime/durable-object.ts b/packages/contrail/src/core/realtime/durable-object.ts similarity index 100% rename from src/core/realtime/durable-object.ts rename to packages/contrail/src/core/realtime/durable-object.ts diff --git a/src/core/realtime/in-memory.ts b/packages/contrail/src/core/realtime/in-memory.ts similarity index 100% rename from src/core/realtime/in-memory.ts rename to packages/contrail/src/core/realtime/in-memory.ts diff --git a/src/core/realtime/index.ts b/packages/contrail/src/core/realtime/index.ts similarity index 100% rename from src/core/realtime/index.ts rename to packages/contrail/src/core/realtime/index.ts diff --git a/src/core/realtime/merge.ts b/packages/contrail/src/core/realtime/merge.ts similarity index 100% rename from src/core/realtime/merge.ts rename to packages/contrail/src/core/realtime/merge.ts diff --git a/src/core/realtime/publishing-adapter.ts b/packages/contrail/src/core/realtime/publishing-adapter.ts similarity index 100% rename from src/core/realtime/publishing-adapter.ts rename to packages/contrail/src/core/realtime/publishing-adapter.ts diff --git a/src/core/realtime/query-filter.ts b/packages/contrail/src/core/realtime/query-filter.ts similarity index 100% rename from src/core/realtime/query-filter.ts rename to packages/contrail/src/core/realtime/query-filter.ts diff --git a/src/core/realtime/resolve.ts b/packages/contrail/src/core/realtime/resolve.ts similarity index 100% rename from src/core/realtime/resolve.ts rename to packages/contrail/src/core/realtime/resolve.ts diff --git a/src/core/realtime/router.ts b/packages/contrail/src/core/realtime/router.ts similarity index 100% rename from src/core/realtime/router.ts rename to packages/contrail/src/core/realtime/router.ts diff --git a/src/core/realtime/sse.ts b/packages/contrail/src/core/realtime/sse.ts similarity index 100% rename from src/core/realtime/sse.ts rename to packages/contrail/src/core/realtime/sse.ts diff --git a/src/core/realtime/ticket.ts b/packages/contrail/src/core/realtime/ticket.ts similarity index 100% rename from src/core/realtime/ticket.ts rename to packages/contrail/src/core/realtime/ticket.ts diff --git a/src/core/realtime/types.ts b/packages/contrail/src/core/realtime/types.ts similarity index 100% rename from src/core/realtime/types.ts rename to packages/contrail/src/core/realtime/types.ts diff --git a/src/core/realtime/websocket.ts b/packages/contrail/src/core/realtime/websocket.ts similarity index 100% rename from src/core/realtime/websocket.ts rename to packages/contrail/src/core/realtime/websocket.ts diff --git a/src/core/router/admin.ts b/packages/contrail/src/core/router/admin.ts similarity index 100% rename from src/core/router/admin.ts rename to packages/contrail/src/core/router/admin.ts diff --git a/src/core/router/collection.ts b/packages/contrail/src/core/router/collection.ts similarity index 100% rename from src/core/router/collection.ts rename to packages/contrail/src/core/router/collection.ts diff --git a/src/core/router/feed.ts b/packages/contrail/src/core/router/feed.ts similarity index 100% rename from src/core/router/feed.ts rename to packages/contrail/src/core/router/feed.ts diff --git a/src/core/router/helpers.ts b/packages/contrail/src/core/router/helpers.ts similarity index 100% rename from src/core/router/helpers.ts rename to packages/contrail/src/core/router/helpers.ts diff --git a/src/core/router/hydrate.ts b/packages/contrail/src/core/router/hydrate.ts similarity index 100% rename from src/core/router/hydrate.ts rename to packages/contrail/src/core/router/hydrate.ts diff --git a/src/core/router/index.ts b/packages/contrail/src/core/router/index.ts similarity index 100% rename from src/core/router/index.ts rename to packages/contrail/src/core/router/index.ts diff --git a/src/core/router/notify.ts b/packages/contrail/src/core/router/notify.ts similarity index 100% rename from src/core/router/notify.ts rename to packages/contrail/src/core/router/notify.ts diff --git a/src/core/router/profiles.ts b/packages/contrail/src/core/router/profiles.ts similarity index 100% rename from src/core/router/profiles.ts rename to packages/contrail/src/core/router/profiles.ts diff --git a/src/core/search.ts b/packages/contrail/src/core/search.ts similarity index 100% rename from src/core/search.ts rename to packages/contrail/src/core/search.ts diff --git a/src/core/spaces/acl.ts b/packages/contrail/src/core/spaces/acl.ts similarity index 100% rename from src/core/spaces/acl.ts rename to packages/contrail/src/core/spaces/acl.ts diff --git a/src/core/spaces/adapter.ts b/packages/contrail/src/core/spaces/adapter.ts similarity index 100% rename from src/core/spaces/adapter.ts rename to packages/contrail/src/core/spaces/adapter.ts diff --git a/src/core/spaces/auth.ts b/packages/contrail/src/core/spaces/auth.ts similarity index 100% rename from src/core/spaces/auth.ts rename to packages/contrail/src/core/spaces/auth.ts diff --git a/src/core/spaces/blob-adapter.ts b/packages/contrail/src/core/spaces/blob-adapter.ts similarity index 100% rename from src/core/spaces/blob-adapter.ts rename to packages/contrail/src/core/spaces/blob-adapter.ts diff --git a/src/core/spaces/blob-gc.ts b/packages/contrail/src/core/spaces/blob-gc.ts similarity index 100% rename from src/core/spaces/blob-gc.ts rename to packages/contrail/src/core/spaces/blob-gc.ts diff --git a/src/core/spaces/blob-refs.ts b/packages/contrail/src/core/spaces/blob-refs.ts similarity index 100% rename from src/core/spaces/blob-refs.ts rename to packages/contrail/src/core/spaces/blob-refs.ts diff --git a/src/core/spaces/in-process.ts b/packages/contrail/src/core/spaces/in-process.ts similarity index 100% rename from src/core/spaces/in-process.ts rename to packages/contrail/src/core/spaces/in-process.ts diff --git a/src/core/spaces/router.ts b/packages/contrail/src/core/spaces/router.ts similarity index 100% rename from src/core/spaces/router.ts rename to packages/contrail/src/core/spaces/router.ts diff --git a/src/core/spaces/schema.ts b/packages/contrail/src/core/spaces/schema.ts similarity index 100% rename from src/core/spaces/schema.ts rename to packages/contrail/src/core/spaces/schema.ts diff --git a/src/core/spaces/tid.ts b/packages/contrail/src/core/spaces/tid.ts similarity index 100% rename from src/core/spaces/tid.ts rename to packages/contrail/src/core/spaces/tid.ts diff --git a/src/core/spaces/types.ts b/packages/contrail/src/core/spaces/types.ts similarity index 100% rename from src/core/spaces/types.ts rename to packages/contrail/src/core/spaces/types.ts diff --git a/src/core/spaces/uri.ts b/packages/contrail/src/core/spaces/uri.ts similarity index 100% rename from src/core/spaces/uri.ts rename to packages/contrail/src/core/spaces/uri.ts diff --git a/src/core/types.ts b/packages/contrail/src/core/types.ts similarity index 100% rename from src/core/types.ts rename to packages/contrail/src/core/types.ts diff --git a/src/index.ts b/packages/contrail/src/index.ts similarity index 100% rename from src/index.ts rename to packages/contrail/src/index.ts diff --git a/src/server.ts b/packages/contrail/src/server.ts similarity index 100% rename from src/server.ts rename to packages/contrail/src/server.ts diff --git a/tests/community-delegation.test.ts b/packages/contrail/tests/community-delegation.test.ts similarity index 100% rename from tests/community-delegation.test.ts rename to packages/contrail/tests/community-delegation.test.ts diff --git a/tests/community-e2e.test.ts b/packages/contrail/tests/community-e2e.test.ts similarity index 100% rename from tests/community-e2e.test.ts rename to packages/contrail/tests/community-e2e.test.ts diff --git a/tests/community-mint.test.ts b/packages/contrail/tests/community-mint.test.ts similarity index 100% rename from tests/community-mint.test.ts rename to packages/contrail/tests/community-mint.test.ts diff --git a/tests/community-publishing.test.ts b/packages/contrail/tests/community-publishing.test.ts similarity index 100% rename from tests/community-publishing.test.ts rename to packages/contrail/tests/community-publishing.test.ts diff --git a/tests/contrail-entry.test.ts b/packages/contrail/tests/contrail-entry.test.ts similarity index 100% rename from tests/contrail-entry.test.ts rename to packages/contrail/tests/contrail-entry.test.ts diff --git a/tests/dialect.test.ts b/packages/contrail/tests/dialect.test.ts similarity index 100% rename from tests/dialect.test.ts rename to packages/contrail/tests/dialect.test.ts diff --git a/tests/helpers.test.ts b/packages/contrail/tests/helpers.test.ts similarity index 100% rename from tests/helpers.test.ts rename to packages/contrail/tests/helpers.test.ts diff --git a/tests/helpers.ts b/packages/contrail/tests/helpers.ts similarity index 100% rename from tests/helpers.ts rename to packages/contrail/tests/helpers.ts diff --git a/tests/hydrate.test.ts b/packages/contrail/tests/hydrate.test.ts similarity index 100% rename from tests/hydrate.test.ts rename to packages/contrail/tests/hydrate.test.ts diff --git a/tests/invite-unified.test.ts b/packages/contrail/tests/invite-unified.test.ts similarity index 100% rename from tests/invite-unified.test.ts rename to packages/contrail/tests/invite-unified.test.ts diff --git a/tests/notify.test.ts b/packages/contrail/tests/notify.test.ts similarity index 100% rename from tests/notify.test.ts rename to packages/contrail/tests/notify.test.ts diff --git a/tests/persistent.test.ts b/packages/contrail/tests/persistent.test.ts similarity index 100% rename from tests/persistent.test.ts rename to packages/contrail/tests/persistent.test.ts diff --git a/tests/postgres-e2e.test.ts b/packages/contrail/tests/postgres-e2e.test.ts similarity index 100% rename from tests/postgres-e2e.test.ts rename to packages/contrail/tests/postgres-e2e.test.ts diff --git a/tests/postgres.test.ts b/packages/contrail/tests/postgres.test.ts similarity index 100% rename from tests/postgres.test.ts rename to packages/contrail/tests/postgres.test.ts diff --git a/tests/realtime-durable-object.test.ts b/packages/contrail/tests/realtime-durable-object.test.ts similarity index 100% rename from tests/realtime-durable-object.test.ts rename to packages/contrail/tests/realtime-durable-object.test.ts diff --git a/tests/realtime-e2e.test.ts b/packages/contrail/tests/realtime-e2e.test.ts similarity index 100% rename from tests/realtime-e2e.test.ts rename to packages/contrail/tests/realtime-e2e.test.ts diff --git a/tests/realtime-foundations.test.ts b/packages/contrail/tests/realtime-foundations.test.ts similarity index 100% rename from tests/realtime-foundations.test.ts rename to packages/contrail/tests/realtime-foundations.test.ts diff --git a/tests/realtime-public.test.ts b/packages/contrail/tests/realtime-public.test.ts similarity index 100% rename from tests/realtime-public.test.ts rename to packages/contrail/tests/realtime-public.test.ts diff --git a/tests/records.test.ts b/packages/contrail/tests/records.test.ts similarity index 100% rename from tests/records.test.ts rename to packages/contrail/tests/records.test.ts diff --git a/tests/schema.test.ts b/packages/contrail/tests/schema.test.ts similarity index 100% rename from tests/schema.test.ts rename to packages/contrail/tests/schema.test.ts diff --git a/tests/search.test.ts b/packages/contrail/tests/search.test.ts similarity index 100% rename from tests/search.test.ts rename to packages/contrail/tests/search.test.ts diff --git a/tests/spaces-acl.test.ts b/packages/contrail/tests/spaces-acl.test.ts similarity index 100% rename from tests/spaces-acl.test.ts rename to packages/contrail/tests/spaces-acl.test.ts diff --git a/tests/spaces-blobs.test.ts b/packages/contrail/tests/spaces-blobs.test.ts similarity index 100% rename from tests/spaces-blobs.test.ts rename to packages/contrail/tests/spaces-blobs.test.ts diff --git a/tests/spaces-e2e.test.ts b/packages/contrail/tests/spaces-e2e.test.ts similarity index 100% rename from tests/spaces-e2e.test.ts rename to packages/contrail/tests/spaces-e2e.test.ts diff --git a/tests/spaces-invites.test.ts b/packages/contrail/tests/spaces-invites.test.ts similarity index 100% rename from tests/spaces-invites.test.ts rename to packages/contrail/tests/spaces-invites.test.ts diff --git a/tests/types.test.ts b/packages/contrail/tests/types.test.ts similarity index 100% rename from tests/types.test.ts rename to packages/contrail/tests/types.test.ts diff --git a/packages/contrail/tsconfig.build.json b/packages/contrail/tsconfig.build.json new file mode 100644 index 0000000..62b4219 --- /dev/null +++ b/packages/contrail/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types", "node"] + }, + "include": ["src"] +} diff --git a/packages/contrail/tsconfig.json b/packages/contrail/tsconfig.json new file mode 100644 index 0000000..80adad9 --- /dev/null +++ b/packages/contrail/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"], + "exclude": ["src/adapters/sqlite.ts"] +} diff --git a/tsup.config.ts b/packages/contrail/tsup.config.ts similarity index 76% rename from tsup.config.ts rename to packages/contrail/tsup.config.ts index 1bbaa88..e1ce7fa 100644 --- a/tsup.config.ts +++ b/packages/contrail/tsup.config.ts @@ -4,10 +4,6 @@ export default defineConfig({ entry: [ "src/index.ts", "src/server.ts", - "src/generate.ts", - "src/publish.ts", - "src/sync/index.ts", - "src/sync/cache-idb.ts", "src/adapters/sqlite.ts", "src/adapters/postgres.ts", ], diff --git a/vitest.config.ts b/packages/contrail/vitest.config.ts similarity index 100% rename from vitest.config.ts rename to packages/contrail/vitest.config.ts diff --git a/packages/lexicons/README.md b/packages/lexicons/README.md new file mode 100644 index 0000000..84776a1 --- /dev/null +++ b/packages/lexicons/README.md @@ -0,0 +1,39 @@ +# @atmo-dev/contrail-lexicons + +Generate atproto lexicon JSON from a [contrail](https://www.npmjs.com/package/@atmo-dev/contrail) config, and (optionally) drive the full `@atcute/lex-cli` pull + type-generation pipeline from a single command. + +## Install + +```bash +pnpm add -D @atmo-dev/contrail-lexicons @atcute/lex-cli +``` + +`@atcute/lex-cli` is a peer dep — you pin the version. + +## CLI + +```bash +contrail-lex generate # emit lexicon JSON from your Contrail config +contrail-lex pull # wraps `lex-cli pull` +contrail-lex types # wraps `lex-cli generate` +contrail-lex all # generate → pull → generate → pull → types +contrail-lex all --no-types # skip the final type-generation step +``` + +The CLI auto-detects your Contrail config at `contrail.config.ts`, `app/config.ts`, or `src/lib/contrail/config.ts` (first match wins). Override with `--config `. + +For anything the `all` subcommand doesn't cover — custom output dirs, extra pull sources, multiple `lex.config.js` files — call `lex-cli` directly. This package doesn't hide or replace it. + +## Programmatic API + +```ts +import { generateLexicons } from "@atmo-dev/contrail-lexicons"; + +const generated = generateLexicons({ + config, + rootDir: process.cwd(), + outputDir: "lexicons-generated", +}); +``` + +See also: `extractXrpcMethods`, `listXrpcMethods`, `publishLexicons`. diff --git a/lexicon-templates/community/adopt.json b/packages/lexicons/lexicon-templates/community/adopt.json similarity index 100% rename from lexicon-templates/community/adopt.json rename to packages/lexicons/lexicon-templates/community/adopt.json diff --git a/lexicon-templates/community/defs.json b/packages/lexicons/lexicon-templates/community/defs.json similarity index 100% rename from lexicon-templates/community/defs.json rename to packages/lexicons/lexicon-templates/community/defs.json diff --git a/lexicon-templates/community/delete.json b/packages/lexicons/lexicon-templates/community/delete.json similarity index 100% rename from lexicon-templates/community/delete.json rename to packages/lexicons/lexicon-templates/community/delete.json diff --git a/lexicon-templates/community/deleteRecord.json b/packages/lexicons/lexicon-templates/community/deleteRecord.json similarity index 100% rename from lexicon-templates/community/deleteRecord.json rename to packages/lexicons/lexicon-templates/community/deleteRecord.json diff --git a/lexicon-templates/community/getHealth.json b/packages/lexicons/lexicon-templates/community/getHealth.json similarity index 100% rename from lexicon-templates/community/getHealth.json rename to packages/lexicons/lexicon-templates/community/getHealth.json diff --git a/lexicon-templates/community/list.json b/packages/lexicons/lexicon-templates/community/list.json similarity index 100% rename from lexicon-templates/community/list.json rename to packages/lexicons/lexicon-templates/community/list.json diff --git a/lexicon-templates/community/mint.json b/packages/lexicons/lexicon-templates/community/mint.json similarity index 100% rename from lexicon-templates/community/mint.json rename to packages/lexicons/lexicon-templates/community/mint.json diff --git a/lexicon-templates/community/putRecord.json b/packages/lexicons/lexicon-templates/community/putRecord.json similarity index 100% rename from lexicon-templates/community/putRecord.json rename to packages/lexicons/lexicon-templates/community/putRecord.json diff --git a/lexicon-templates/community/reauth.json b/packages/lexicons/lexicon-templates/community/reauth.json similarity index 100% rename from lexicon-templates/community/reauth.json rename to packages/lexicons/lexicon-templates/community/reauth.json diff --git a/lexicon-templates/community/space/create.json b/packages/lexicons/lexicon-templates/community/space/create.json similarity index 100% rename from lexicon-templates/community/space/create.json rename to packages/lexicons/lexicon-templates/community/space/create.json diff --git a/lexicon-templates/community/space/delete.json b/packages/lexicons/lexicon-templates/community/space/delete.json similarity index 100% rename from lexicon-templates/community/space/delete.json rename to packages/lexicons/lexicon-templates/community/space/delete.json diff --git a/lexicon-templates/community/space/deleteRecord.json b/packages/lexicons/lexicon-templates/community/space/deleteRecord.json similarity index 100% rename from lexicon-templates/community/space/deleteRecord.json rename to packages/lexicons/lexicon-templates/community/space/deleteRecord.json diff --git a/lexicon-templates/community/space/grant.json b/packages/lexicons/lexicon-templates/community/space/grant.json similarity index 100% rename from lexicon-templates/community/space/grant.json rename to packages/lexicons/lexicon-templates/community/space/grant.json diff --git a/lexicon-templates/community/space/listMembers.json b/packages/lexicons/lexicon-templates/community/space/listMembers.json similarity index 100% rename from lexicon-templates/community/space/listMembers.json rename to packages/lexicons/lexicon-templates/community/space/listMembers.json diff --git a/lexicon-templates/community/space/putRecord.json b/packages/lexicons/lexicon-templates/community/space/putRecord.json similarity index 100% rename from lexicon-templates/community/space/putRecord.json rename to packages/lexicons/lexicon-templates/community/space/putRecord.json diff --git a/lexicon-templates/community/space/resync.json b/packages/lexicons/lexicon-templates/community/space/resync.json similarity index 100% rename from lexicon-templates/community/space/resync.json rename to packages/lexicons/lexicon-templates/community/space/resync.json diff --git a/lexicon-templates/community/space/revoke.json b/packages/lexicons/lexicon-templates/community/space/revoke.json similarity index 100% rename from lexicon-templates/community/space/revoke.json rename to packages/lexicons/lexicon-templates/community/space/revoke.json diff --git a/lexicon-templates/community/space/setAccessLevel.json b/packages/lexicons/lexicon-templates/community/space/setAccessLevel.json similarity index 100% rename from lexicon-templates/community/space/setAccessLevel.json rename to packages/lexicons/lexicon-templates/community/space/setAccessLevel.json diff --git a/lexicon-templates/invite/create.json b/packages/lexicons/lexicon-templates/invite/create.json similarity index 100% rename from lexicon-templates/invite/create.json rename to packages/lexicons/lexicon-templates/invite/create.json diff --git a/lexicon-templates/invite/defs.json b/packages/lexicons/lexicon-templates/invite/defs.json similarity index 100% rename from lexicon-templates/invite/defs.json rename to packages/lexicons/lexicon-templates/invite/defs.json diff --git a/lexicon-templates/invite/list.json b/packages/lexicons/lexicon-templates/invite/list.json similarity index 100% rename from lexicon-templates/invite/list.json rename to packages/lexicons/lexicon-templates/invite/list.json diff --git a/lexicon-templates/invite/redeem.json b/packages/lexicons/lexicon-templates/invite/redeem.json similarity index 100% rename from lexicon-templates/invite/redeem.json rename to packages/lexicons/lexicon-templates/invite/redeem.json diff --git a/lexicon-templates/invite/revoke.json b/packages/lexicons/lexicon-templates/invite/revoke.json similarity index 100% rename from lexicon-templates/invite/revoke.json rename to packages/lexicons/lexicon-templates/invite/revoke.json diff --git a/lexicon-templates/realtime/subscribe.json b/packages/lexicons/lexicon-templates/realtime/subscribe.json similarity index 100% rename from lexicon-templates/realtime/subscribe.json rename to packages/lexicons/lexicon-templates/realtime/subscribe.json diff --git a/lexicon-templates/realtime/ticket.json b/packages/lexicons/lexicon-templates/realtime/ticket.json similarity index 100% rename from lexicon-templates/realtime/ticket.json rename to packages/lexicons/lexicon-templates/realtime/ticket.json diff --git a/lexicon-templates/spaces/addMember.json b/packages/lexicons/lexicon-templates/spaces/addMember.json similarity index 100% rename from lexicon-templates/spaces/addMember.json rename to packages/lexicons/lexicon-templates/spaces/addMember.json diff --git a/lexicon-templates/spaces/createSpace.json b/packages/lexicons/lexicon-templates/spaces/createSpace.json similarity index 100% rename from lexicon-templates/spaces/createSpace.json rename to packages/lexicons/lexicon-templates/spaces/createSpace.json diff --git a/lexicon-templates/spaces/defs.json b/packages/lexicons/lexicon-templates/spaces/defs.json similarity index 100% rename from lexicon-templates/spaces/defs.json rename to packages/lexicons/lexicon-templates/spaces/defs.json diff --git a/lexicon-templates/spaces/deleteRecord.json b/packages/lexicons/lexicon-templates/spaces/deleteRecord.json similarity index 100% rename from lexicon-templates/spaces/deleteRecord.json rename to packages/lexicons/lexicon-templates/spaces/deleteRecord.json diff --git a/lexicon-templates/spaces/ext/whoami.json b/packages/lexicons/lexicon-templates/spaces/ext/whoami.json similarity index 100% rename from lexicon-templates/spaces/ext/whoami.json rename to packages/lexicons/lexicon-templates/spaces/ext/whoami.json diff --git a/lexicon-templates/spaces/getBlob.json b/packages/lexicons/lexicon-templates/spaces/getBlob.json similarity index 100% rename from lexicon-templates/spaces/getBlob.json rename to packages/lexicons/lexicon-templates/spaces/getBlob.json diff --git a/lexicon-templates/spaces/getRecord.json b/packages/lexicons/lexicon-templates/spaces/getRecord.json similarity index 100% rename from lexicon-templates/spaces/getRecord.json rename to packages/lexicons/lexicon-templates/spaces/getRecord.json diff --git a/lexicon-templates/spaces/getSpace.json b/packages/lexicons/lexicon-templates/spaces/getSpace.json similarity index 100% rename from lexicon-templates/spaces/getSpace.json rename to packages/lexicons/lexicon-templates/spaces/getSpace.json diff --git a/lexicon-templates/spaces/leaveSpace.json b/packages/lexicons/lexicon-templates/spaces/leaveSpace.json similarity index 100% rename from lexicon-templates/spaces/leaveSpace.json rename to packages/lexicons/lexicon-templates/spaces/leaveSpace.json diff --git a/lexicon-templates/spaces/listBlobs.json b/packages/lexicons/lexicon-templates/spaces/listBlobs.json similarity index 100% rename from lexicon-templates/spaces/listBlobs.json rename to packages/lexicons/lexicon-templates/spaces/listBlobs.json diff --git a/lexicon-templates/spaces/listMembers.json b/packages/lexicons/lexicon-templates/spaces/listMembers.json similarity index 100% rename from lexicon-templates/spaces/listMembers.json rename to packages/lexicons/lexicon-templates/spaces/listMembers.json diff --git a/lexicon-templates/spaces/listRecords.json b/packages/lexicons/lexicon-templates/spaces/listRecords.json similarity index 100% rename from lexicon-templates/spaces/listRecords.json rename to packages/lexicons/lexicon-templates/spaces/listRecords.json diff --git a/lexicon-templates/spaces/listSpaces.json b/packages/lexicons/lexicon-templates/spaces/listSpaces.json similarity index 100% rename from lexicon-templates/spaces/listSpaces.json rename to packages/lexicons/lexicon-templates/spaces/listSpaces.json diff --git a/lexicon-templates/spaces/putRecord.json b/packages/lexicons/lexicon-templates/spaces/putRecord.json similarity index 100% rename from lexicon-templates/spaces/putRecord.json rename to packages/lexicons/lexicon-templates/spaces/putRecord.json diff --git a/lexicon-templates/spaces/removeMember.json b/packages/lexicons/lexicon-templates/spaces/removeMember.json similarity index 100% rename from lexicon-templates/spaces/removeMember.json rename to packages/lexicons/lexicon-templates/spaces/removeMember.json diff --git a/lexicon-templates/spaces/uploadBlob.json b/packages/lexicons/lexicon-templates/spaces/uploadBlob.json similarity index 100% rename from lexicon-templates/spaces/uploadBlob.json rename to packages/lexicons/lexicon-templates/spaces/uploadBlob.json diff --git a/packages/lexicons/package.json b/packages/lexicons/package.json new file mode 100644 index 0000000..2225f69 --- /dev/null +++ b/packages/lexicons/package.json @@ -0,0 +1,59 @@ +{ + "name": "@atmo-dev/contrail-lexicons", + "version": "0.1.1", + "description": "Generate atproto lexicon JSON (and optionally TypeScript types via @atcute/lex-cli) from a Contrail config.", + "type": "module", + "files": [ + "dist", + "lexicon-templates/**/*.json" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./generate": { + "types": "./dist/generate.d.ts", + "import": "./dist/generate.js" + }, + "./publish": { + "types": "./dist/publish.d.ts", + "import": "./dist/publish.js" + }, + "./lexicon-templates/*.json": "./lexicon-templates/*.json" + }, + "bin": { + "contrail-lex": "./dist/cli.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/flo-bit/contrail.git", + "directory": "packages/lexicons" + }, + "keywords": [ + "atproto", + "contrail", + "lexicon", + "codegen" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@atcute/lex-cli": "^2.5.3" + }, + "devDependencies": { + "@atmo-dev/contrail": "workspace:*", + "@types/node": "^25.5.0", + "tsup": "^8.5.0", + "typescript": "^5.7.3", + "vitest": "^4.1.0" + }, + "peerDependencies": { + "@atmo-dev/contrail": "workspace:*" + }, + "license": "MIT" +} diff --git a/packages/lexicons/src/cli.ts b/packages/lexicons/src/cli.ts new file mode 100644 index 0000000..fb29109 --- /dev/null +++ b/packages/lexicons/src/cli.ts @@ -0,0 +1,172 @@ +#!/usr/bin/env node +/** + * contrail-lex — CLI for generating lexicons from a Contrail config and + * (optionally) typegenning them via @atcute/lex-cli. + * + * Usage from a consumer project: + * + * contrail-lex generate [--config ] # lexicon JSON only + * contrail-lex pull # wraps `lex-cli pull` + * contrail-lex types # wraps `lex-cli generate` + * contrail-lex all [--no-types] [--config ...] # generate → pull → types + * + * Looks for a user config via --config , else ./contrail.config.ts, + * ./app/config.ts, ./src/lib/contrail/config.ts (first match wins). The + * config file must default-export or named-export `config: ContrailConfig`. + */ +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { generateLexicons } from "./generate.js"; + +type Subcommand = "generate" | "pull" | "types" | "all" | "help"; + +const USAGE = `contrail-lex [options] + +Subcommands: + generate Emit lexicon JSON from Contrail config + pull Pull external lexicons (wraps \`lex-cli pull\`) + types Generate TS types from lexicon JSON (wraps \`lex-cli generate\`) + all generate → pull → generate → pull → types (full pipeline) + help Print this message + +Options: + --config Path to Contrail config file (TS or JS, default export + or named \`config\`). Default: auto-detect. + --root Project root (where lexicons/ and node_modules/ live). + Default: CWD. + --no-types In \`all\`, skip the final type-generation step. +`; + +function parseArgs(argv: string[]): { + cmd: Subcommand; + config?: string; + root: string; + withTypes: boolean; +} { + const args = argv.slice(2); + const cmd = (args.shift() ?? "help") as Subcommand; + let config: string | undefined; + let root = process.cwd(); + let withTypes = true; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === "--config") config = args[++i]; + else if (a === "--root") root = args[++i]; + else if (a === "--no-types") withTypes = false; + else if (a === "-h" || a === "--help") return { cmd: "help", root, withTypes: true }; + } + return { cmd, config, root, withTypes }; +} + +function findConfigFile(root: string, explicit?: string): string | null { + if (explicit) { + const p = resolve(root, explicit); + return existsSync(p) ? p : null; + } + const candidates = [ + "contrail.config.ts", + "contrail.config.js", + "app/config.ts", + "src/lib/contrail/config.ts", + ]; + for (const c of candidates) { + const p = join(root, c); + if (existsSync(p)) return p; + } + return null; +} + +async function loadConfig(path: string): Promise { + // TS files are loaded via `tsx` if available; otherwise fall back to the + // Node loader (works for .js). We use dynamic import. + if (path.endsWith(".ts") || path.endsWith(".mts")) { + // `tsx` registers a hook when invoked via `tsx `. If this CLI is + // run through tsx (e.g. `npx tsx contrail-lex ...`), the import below + // will just work. Otherwise we spawn a tsx child below — but dynamic + // import is the preferred path for a programmatic config load. + const mod = await import(pathToFileURL(path).href); + return mod.config ?? mod.default; + } + const mod = await import(pathToFileURL(path).href); + return mod.config ?? mod.default; +} + +function runLexCli(args: string[], cwd: string): number { + const result = spawnSync("npx", ["lex-cli", ...args], { + cwd, + stdio: "inherit", + shell: process.platform === "win32", + }); + return result.status ?? 1; +} + +async function cmdGenerate(configPath: string, root: string): Promise { + const config = await loadConfig(configPath); + if (!config || typeof config !== "object") { + throw new Error(`Config at ${configPath} did not export a \`config\` object`); + } + generateLexicons({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: config as any, + rootDir: root, + outputDir: join(root, "lexicons-generated"), + writeRuntimeFiles: true, + }); +} + +async function main(): Promise { + const { cmd, config, root, withTypes } = parseArgs(process.argv); + + if (cmd === "help") { + process.stdout.write(USAGE); + return 0; + } + + if (cmd === "pull") return runLexCli(["pull"], root); + if (cmd === "types") return runLexCli(["generate"], root); + + const configPath = findConfigFile(root, config); + if (!configPath) { + console.error( + "Could not find a Contrail config. Pass --config or place one at\n" + + " contrail.config.ts | app/config.ts | src/lib/contrail/config.ts" + ); + return 1; + } + + if (cmd === "generate") { + await cmdGenerate(configPath, root); + return 0; + } + + if (cmd === "all") { + // Two-pass generate + pull is deliberate: the first `generate` emits any + // record-type placeholders that `lex-cli pull` resolves, and the second + // pass picks up the pulled data. Mirrors the historical `generate:pull` + // script shape. + await cmdGenerate(configPath, root); + let rc = runLexCli(["pull"], root); + if (rc !== 0) return rc; + await cmdGenerate(configPath, root); + rc = runLexCli(["pull"], root); + if (rc !== 0) return rc; + if (withTypes) { + rc = runLexCli(["generate"], root); + if (rc !== 0) return rc; + } + return 0; + } + + console.error(USAGE); + return 1; +} + +main().then( + (code) => process.exit(code), + (err) => { + console.error(err); + process.exit(1); + } +); diff --git a/src/generate.ts b/packages/lexicons/src/generate.ts similarity index 98% rename from src/generate.ts rename to packages/lexicons/src/generate.ts index bf9ed21..276798d 100644 --- a/src/generate.ts +++ b/packages/lexicons/src/generate.ts @@ -7,7 +7,7 @@ import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from "fs"; import { join } from "path"; -import type { ContrailConfig } from "./core/types"; +import type { ContrailConfig } from "@atmo-dev/contrail"; /** Return the sorted list of XRPC method NSIDs (queries + procedures) in a * generated lexicon set — the same list that ends up in `.permissionSet`'s @@ -68,9 +68,16 @@ function findTemplatesDir( rootDir: string, module: "spaces" | "community" | "realtime" | "invite" ): string | null { + // Templates are shipped with @atmo-dev/contrail-lexicons. Look in the + // user project's node_modules first, then fall back to the package's own + // directory (for development in this monorepo, where the templates live + // next to this file). const candidates = [ join(rootDir, "lexicon-templates", module), - join(rootDir, "node_modules/@atmo-dev/contrail/lexicon-templates", module), + join(rootDir, "node_modules/@atmo-dev/contrail-lexicons/lexicon-templates", module), + // When running from the package's own dist/ (ESM), __dirname is unavailable; + // rely on import.meta.url. + new URL(`../lexicon-templates/${module}`, import.meta.url).pathname, ]; for (const p of candidates) { if (existsSync(p)) return p; diff --git a/packages/lexicons/src/index.ts b/packages/lexicons/src/index.ts new file mode 100644 index 0000000..18dd5b6 --- /dev/null +++ b/packages/lexicons/src/index.ts @@ -0,0 +1,9 @@ +export { + generateLexicons, + extractXrpcMethods, + listXrpcMethods, +} from "./generate.js"; +export type { GenerateOptions } from "./generate.js"; + +export { publishLexicons, nsidAuthority, nsidRootDomain } from "./publish.js"; +export type { PublishOptions, Session } from "./publish.js"; diff --git a/src/publish.ts b/packages/lexicons/src/publish.ts similarity index 100% rename from src/publish.ts rename to packages/lexicons/src/publish.ts diff --git a/tests/generate.test.ts b/packages/lexicons/tests/generate.test.ts similarity index 99% rename from tests/generate.test.ts rename to packages/lexicons/tests/generate.test.ts index 2a3456b..28460ff 100644 --- a/tests/generate.test.ts +++ b/packages/lexicons/tests/generate.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeAll } from "vitest"; import { join } from "path"; import { generateLexicons, extractXrpcMethods, listXrpcMethods } from "../src/generate"; -import type { ContrailConfig } from "../src/core/types"; +import type { ContrailConfig } from "@atmo-dev/contrail"; const ROOT_DIR = join(__dirname, ".."); diff --git a/packages/lexicons/tsconfig.build.json b/packages/lexicons/tsconfig.build.json new file mode 100644 index 0000000..374bac5 --- /dev/null +++ b/packages/lexicons/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/lexicons/tsconfig.json b/packages/lexicons/tsconfig.json new file mode 100644 index 0000000..822a105 --- /dev/null +++ b/packages/lexicons/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src", "tests"] +} diff --git a/packages/lexicons/tsup.config.ts b/packages/lexicons/tsup.config.ts new file mode 100644 index 0000000..b478313 --- /dev/null +++ b/packages/lexicons/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: [ + "src/index.ts", + "src/generate.ts", + "src/publish.ts", + "src/cli.ts", + ], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + tsconfig: "tsconfig.build.json", +}); diff --git a/packages/lexicons/vitest.config.ts b/packages/lexicons/vitest.config.ts new file mode 100644 index 0000000..19384e8 --- /dev/null +++ b/packages/lexicons/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/packages/sync/README.md b/packages/sync/README.md new file mode 100644 index 0000000..42c66fc --- /dev/null +++ b/packages/sync/README.md @@ -0,0 +1,28 @@ +# @atmo-dev/contrail-sync + +Client-side reactive watch-store over [contrail](https://www.npmjs.com/package/@atmo-dev/contrail)'s `watchRecords` endpoints. + +- SSE (default) or WebSocket transports. WS handshake produces a watch-scoped ticket so Cloudflare Durable Objects can hibernate idle connections. +- Optimistic updates (`addOptimistic` / `markFailed` / `removeOptimistic`). +- Automatic reconciliation across reconnects: stale records stay visible until a fresh snapshot arrives, then entries the server didn't re-send are evicted. +- Optional persistent cache (IndexedDB adapter included) for instant first paint. + +```ts +import { createWatchStore } from "@atmo-dev/contrail-sync"; +import { createIndexedDBCache } from "@atmo-dev/contrail-sync/cache-idb"; + +const store = createWatchStore({ + url: "/xrpc/com.example.message.watchRecords?roomUri=at://...", + transport: "ws", + mintTicket: async () => (await fetch("/api/ticket")).then((r) => r.text()), + cache: createIndexedDBCache(), +}); + +store.subscribe(({ records, status }) => { + // re-render +}); + +store.start(); +``` + +Framework-agnostic: wrap the subscribable store in your framework's reactive primitives (Svelte `$state`, React `useSyncExternalStore`, Vue `ref`, etc). diff --git a/packages/sync/package.json b/packages/sync/package.json new file mode 100644 index 0000000..f952514 --- /dev/null +++ b/packages/sync/package.json @@ -0,0 +1,42 @@ +{ + "name": "@atmo-dev/contrail-sync", + "version": "0.1.1", + "description": "Client-side reactive watch-store over contrail's watchRecords endpoints. SSE + WebSocket transports, optimistic updates, optional IndexedDB cache.", + "type": "module", + "sideEffects": false, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./cache-idb": { + "types": "./dist/cache-idb.d.ts", + "import": "./dist/cache-idb.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/flo-bit/contrail.git", + "directory": "packages/sync" + }, + "keywords": [ + "atproto", + "contrail", + "sync", + "realtime", + "watch" + ], + "scripts": { + "build": "tsup", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "tsup": "^8.5.0", + "typescript": "^5.7.3" + }, + "license": "MIT" +} diff --git a/src/sync/cache-idb.ts b/packages/sync/src/cache-idb.ts similarity index 100% rename from src/sync/cache-idb.ts rename to packages/sync/src/cache-idb.ts diff --git a/src/sync/index.ts b/packages/sync/src/index.ts similarity index 100% rename from src/sync/index.ts rename to packages/sync/src/index.ts diff --git a/packages/sync/tsconfig.build.json b/packages/sync/tsconfig.build.json new file mode 100644 index 0000000..6092abf --- /dev/null +++ b/packages/sync/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM"] + }, + "include": ["src"] +} diff --git a/packages/sync/tsconfig.json b/packages/sync/tsconfig.json new file mode 100644 index 0000000..6092abf --- /dev/null +++ b/packages/sync/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM"] + }, + "include": ["src"] +} diff --git a/packages/sync/tsup.config.ts b/packages/sync/tsup.config.ts new file mode 100644 index 0000000..ebf5058 --- /dev/null +++ b/packages/sync/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/cache-idb.ts"], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + tsconfig: "tsconfig.build.json", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76e350c..89a1941 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,96 +7,258 @@ settings: importers: .: + devDependencies: + '@changesets/cli': + specifier: ^2.29.4 + version: 2.31.0(@types/node@25.6.0) + turbo: + specifier: ^2.3.3 + version: 2.9.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + apps/cloudflare-workers: dependencies: + '@atmo-dev/contrail': + specifier: workspace:* + version: link:../../packages/contrail + devDependencies: + '@atcute/lex-cli': + specifier: ^2.5.3 + version: 2.8.1 + '@atcute/lexicon-doc': + specifier: ^2.1.2 + version: 2.2.0 + '@atmo-dev/contrail-lexicons': + specifier: workspace:* + version: link:../../packages/lexicons + '@cloudflare/workers-types': + specifier: ^4.20250124.0 + version: 4.20260424.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + wrangler: + specifier: ^4.63.0 + version: 4.84.1(@cloudflare/workers-types@4.20260424.1) + + apps/group-chat: + dependencies: + '@atcute/jetstream': + specifier: ^1.1.2 + version: 1.1.2 + '@atmo-dev/contrail': + specifier: workspace:* + version: link:../../packages/contrail + '@atmo-dev/contrail-lexicons': + specifier: workspace:* + version: link:../../packages/lexicons + '@atmo-dev/contrail-sync': + specifier: workspace:* + version: link:../../packages/sync + '@foxui/core': + specifier: ^0.9.1 + version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/social': + specifier: ^0.8.10 + version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/time': + specifier: ^0.8.5 + version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + valibot: + specifier: ^1.3.1 + version: 1.3.1(typescript@6.0.3) + devDependencies: '@atcute/atproto': specifier: ^3.1.10 - version: 3.1.10 - '@atcute/cid': - specifier: ^2.4.1 - version: 2.4.1 + version: 3.1.11 + '@atcute/bluesky': + specifier: ^3.3.0 + version: 3.3.3 '@atcute/client': specifier: ^4.2.1 version: 4.2.1 - '@atcute/identity': - specifier: ^1.1.4 - version: 1.1.4 '@atcute/identity-resolver': specifier: ^1.2.2 version: 1.2.2(@atcute/identity@1.1.4) - '@atcute/jetstream': - specifier: ^1.0.2 - version: 1.1.2 + '@atcute/lex-cli': + specifier: ^2.5.3 + version: 2.8.1 + '@atcute/lexicon-doc': + specifier: ^2.1.2 + version: 2.2.0 '@atcute/lexicons': specifier: ^1.2.9 - version: 1.2.9 - '@atcute/xrpc-server': - specifier: ^0.1.12 - version: 0.1.12 - hono: - specifier: ^4.12.8 - version: 4.12.8 + version: 1.3.0 + '@atcute/oauth-node-client': + specifier: ^1.1.0 + version: 1.1.0 + '@atcute/tid': + specifier: ^1.1.2 + version: 1.1.2 + '@cloudflare/workers-types': + specifier: ^4.20260317.1 + version: 4.20260424.1 + '@eslint/compat': + specifier: ^2.0.3 + version: 2.0.5(eslint@10.2.1(jiti@2.6.1)) + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.2.1(jiti@2.6.1)) + '@sveltejs/adapter-cloudflare': + specifier: ^7.2.8 + version: 7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1)) + '@sveltejs/kit': + specifier: ^2.55.0 + version: 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@4.2.4) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + bits-ui: + specifier: ^2.16.4 + version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + eslint: + specifier: ^10.1.0 + version: 10.2.1(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-svelte: + specifier: ^3.16.0 + version: 3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + globals: + specifier: ^17.4.0 + version: 17.5.0 + prettier: + specifier: ^3.8.1 + version: 3.8.3 + prettier-plugin-svelte: + specifier: ^3.5.1 + version: 3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.3(prettier-plugin-svelte@3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)))(prettier@3.8.3) + svelte: + specifier: ^5.55.0 + version: 5.55.5(@typescript-eslint/types@8.59.0) + svelte-check: + specifier: ^4.4.5 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.4 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.2 + version: 6.0.3 + typescript-eslint: + specifier: ^8.57.2 + version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + vite: + specifier: ^8.0.3 + version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + wrangler: + specifier: ^4.78.0 + version: 4.84.1(@cloudflare/workers-types@4.20260424.1) + + apps/postgres: + dependencies: + '@atmo-dev/contrail': + specifier: workspace:* + version: link:../../packages/contrail + pg: + specifier: ^8.20.0 + version: 8.20.0 devDependencies: + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + apps/rsvp-atmo: + dependencies: + '@atmo-dev/contrail': + specifier: workspace:* + version: link:../../packages/contrail + devDependencies: + '@atcute/atproto': + specifier: ^3.1.10 + version: 3.1.11 + '@atcute/client': + specifier: ^4.2.1 + version: 4.2.1 '@atcute/lex-cli': specifier: ^2.5.3 - version: 2.5.3 + version: 2.8.1 '@atcute/lexicon-doc': specifier: ^2.1.2 - version: 2.1.2 - '@changesets/cli': - specifier: ^2.29.4 - version: 2.30.0(@types/node@25.5.0) + version: 2.2.0 + '@atcute/lexicons': + specifier: ^1.2.9 + version: 1.3.0 + '@atmo-dev/contrail-lexicons': + specifier: workspace:* + version: link:../../packages/lexicons '@cloudflare/workers-types': specifier: ^4.20250124.0 - version: 4.20260205.0 + version: 4.20260424.1 '@types/node': specifier: ^25.5.0 - version: 25.5.0 - '@types/pg': - specifier: ^8.20.0 - version: 8.20.0 - pg: - specifier: ^8.20.0 - version: 8.20.0 - tsup: - specifier: ^8.5.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3) + version: 25.6.0 tsx: specifier: ^4.21.0 version: 4.21.0 typescript: specifier: ^5.7.3 version: 5.9.3 - vitest: - specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)) wrangler: specifier: ^4.63.0 - version: 4.63.0(@cloudflare/workers-types@4.20260205.0) + version: 4.84.1(@cloudflare/workers-types@4.20260424.1) - examples/sveltekit-group-chat: + apps/sveltekit-cloudflare-workers: dependencies: '@atcute/jetstream': specifier: ^1.1.2 version: 1.1.2 '@atmo-dev/contrail': specifier: workspace:* - version: link:../.. + version: link:../../packages/contrail + '@atmo-dev/contrail-lexicons': + specifier: workspace:* + version: link:../../packages/lexicons '@foxui/core': specifier: ^0.9.1 - version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@foxui/social': specifier: ^0.8.10 - version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@foxui/time': specifier: ^0.8.5 - version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + version: 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) valibot: specifier: ^1.3.1 version: 1.3.1(typescript@6.0.3) devDependencies: '@atcute/atproto': specifier: ^3.1.10 - version: 3.1.10 + version: 3.1.11 '@atcute/bluesky': specifier: ^3.3.0 version: 3.3.3 @@ -108,10 +270,10 @@ importers: version: 1.2.2(@atcute/identity@1.1.4) '@atcute/lex-cli': specifier: ^2.5.3 - version: 2.5.3 + version: 2.8.1 '@atcute/lexicon-doc': specifier: ^2.1.2 - version: 2.1.2 + version: 2.2.0 '@atcute/lexicons': specifier: ^1.2.9 version: 1.3.0 @@ -123,7 +285,7 @@ importers: version: 1.1.2 '@cloudflare/workers-types': specifier: ^4.20260317.1 - version: 4.20260422.1 + version: 4.20260424.1 '@eslint/compat': specifier: ^2.0.3 version: 2.0.5(eslint@10.2.1(jiti@2.6.1)) @@ -132,22 +294,22 @@ importers: version: 10.0.1(eslint@10.2.1(jiti@2.6.1)) '@sveltejs/adapter-cloudflare': specifier: ^7.2.8 - version: 7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1)) + version: 7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1)) '@sveltejs/kit': specifier: ^2.55.0 - version: 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + version: 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) '@tailwindcss/forms': specifier: ^0.5.11 version: 0.5.11(tailwindcss@4.2.4) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.4(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) bits-ui: specifier: ^2.16.4 - version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + version: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) eslint: specifier: ^10.1.0 version: 10.2.1(jiti@2.6.1) @@ -156,25 +318,25 @@ importers: version: 10.1.8(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.16.0 - version: 3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + version: 3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) globals: specifier: ^17.4.0 version: 17.5.0 prettier: specifier: ^3.8.1 - version: 3.8.1 + version: 3.8.3 prettier-plugin-svelte: specifier: ^3.5.1 - version: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + version: 3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)) prettier-plugin-tailwindcss: specifier: ^0.7.2 - version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)))(prettier@3.8.1) + version: 0.7.3(prettier-plugin-svelte@3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)))(prettier@3.8.3) svelte: specifier: ^5.55.0 - version: 5.55.4(@typescript-eslint/types@8.59.0) + version: 5.55.5(@typescript-eslint/types@8.59.0) svelte-check: specifier: ^4.4.5 - version: 4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3) + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3) tailwindcss: specifier: ^4.2.2 version: 4.2.4 @@ -189,15 +351,95 @@ importers: version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) vite: specifier: ^8.0.3 - version: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) wrangler: specifier: ^4.78.0 - version: 4.84.1(@cloudflare/workers-types@4.20260422.1) + version: 4.84.1(@cloudflare/workers-types@4.20260424.1) -packages: + packages/contrail: + dependencies: + '@atcute/atproto': + specifier: ^3.1.10 + version: 3.1.11 + '@atcute/cid': + specifier: ^2.4.1 + version: 2.4.1 + '@atcute/client': + specifier: ^4.2.1 + version: 4.2.1 + '@atcute/identity': + specifier: ^1.1.4 + version: 1.1.4 + '@atcute/identity-resolver': + specifier: ^1.2.2 + version: 1.2.2(@atcute/identity@1.1.4) + '@atcute/jetstream': + specifier: ^1.0.2 + version: 1.1.2 + '@atcute/lexicons': + specifier: ^1.2.9 + version: 1.3.0 + '@atcute/xrpc-server': + specifier: ^0.1.12 + version: 0.1.12 + hono: + specifier: ^4.12.8 + version: 4.12.15 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250124.0 + version: 4.20260424.1 + '@types/node': + specifier: ^25.5.0 + version: 25.6.0 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + pg: + specifier: ^8.20.0 + version: 8.20.0 + tsup: + specifier: ^8.5.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + + packages/lexicons: + dependencies: + '@atcute/lex-cli': + specifier: ^2.5.3 + version: 2.8.1 + devDependencies: + '@atmo-dev/contrail': + specifier: workspace:* + version: link:../contrail + '@types/node': + specifier: ^25.5.0 + version: 25.6.0 + tsup: + specifier: ^8.5.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) - '@atcute/atproto@3.1.10': - resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} + packages/sync: + devDependencies: + tsup: + specifier: ^8.5.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + +packages: '@atcute/atproto@3.1.11': resolution: {integrity: sha512-yh+ASvA+iHHQij6UeHEKp2+rwvFvQR8A6/5Dk/xvqDslIikWEFx9VlprNwm/clQIPl2bLuQg+LHS8uY9o5nFTA==} @@ -234,12 +476,12 @@ packages: '@atcute/jetstream@1.1.2': resolution: {integrity: sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==} - '@atcute/lex-cli@2.5.3': - resolution: {integrity: sha512-829rvezMOfRkJQRKvupNT8TWT/YYffJ2QsB80D9aPjkXSogrETZA7xZcPaMZBXg+mJaVbLO9S4ThPQmlF0L4UQ==} + '@atcute/lex-cli@2.8.1': + resolution: {integrity: sha512-ab+NpnwvW7gXMjZYYqK3zqhcAPOEYnAaNAXHp28gmL3M33zHgz4IjNQpDnRAjTitCpaJ3KoA3KXlnXNjsV6USg==} hasBin: true - '@atcute/lexicon-doc@2.1.2': - resolution: {integrity: sha512-jTLcOka7b8BIn2SnIZm2m7l6unlJ0gpgW1MnRpSqNbly/AvyRUR/GREduh/QmjT4SGasDm8vdhrM0kOSPFpDLQ==} + '@atcute/lexicon-doc@2.2.0': + resolution: {integrity: sha512-6l4lDlL6KPLDGknRh6HlfGbv98haUgQ0DFaAr1yA4vA95b8YYZUZ4/370ENpiq+d6Lv0tdDAMvOon2mynrp3pQ==} '@atcute/lexicon-resolver@0.1.6': resolution: {integrity: sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==} @@ -247,9 +489,6 @@ packages: '@atcute/identity': ^1.1.0 '@atcute/identity-resolver': ^1.1.3 - '@atcute/lexicons@1.2.9': - resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==} - '@atcute/lexicons@1.3.0': resolution: {integrity: sha512-Eq5y+9onnCXNVUlNiMf31beSXHKqptB7lUo/68YbhlmxdaR7ooywHmahya9goP5AsmlYEA1z+dRPXIDAa9O7cg==} @@ -271,8 +510,8 @@ packages: '@atcute/oauth-types@0.1.1': resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} - '@atcute/repo@0.1.3': - resolution: {integrity: sha512-kN4gkrkQgJwI5xkVQ9zSdI8ULR47uH48EdJdjMvKAWsSDu8zKYd8ZQ6n778qa6o3WYEq02v9QVf7pzeU+3fYVA==} + '@atcute/repo@0.1.4': + resolution: {integrity: sha512-uzbGJkE+1A8UFviosJrtw7HW87u8nCCH1V3yOQ79FPrRhS67EvEHF6GTg4aMkP21ze/pRtttJ1k9pFfDmyTlTg==} '@atcute/tid@1.1.2': resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} @@ -286,11 +525,8 @@ packages: '@atcute/util-fetch@1.0.5': resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==} - '@atcute/util-text@1.1.1': - resolution: {integrity: sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w==} - - '@atcute/util-text@1.2.0': - resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==} + '@atcute/util-text@1.3.1': + resolution: {integrity: sha512-MRgJXkx67znuBXuoAYCJkBZyd3OApL7zZlNf5kXhuoCXcdiu1nblRDycYTADSkym4epBSQWxh26kmI9sewaq6A==} '@atcute/varint@2.0.0': resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==} @@ -306,30 +542,30 @@ packages: resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} engines: {node: '>= 18'} - '@changesets/apply-release-plan@7.1.0': - resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} - '@changesets/assemble-release-plan@6.0.9': - resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.30.0': - resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} hasBin: true - '@changesets/config@3.1.3': - resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - '@changesets/get-dependents-graph@2.1.3': - resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} - '@changesets/get-release-plan@4.0.15': - resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -365,15 +601,6 @@ packages: resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.12.0': - resolution: {integrity: sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: ^1.20260115.0 - peerDependenciesMeta: - workerd: - optional: true - '@cloudflare/unenv-preset@2.16.0': resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==} peerDependencies: @@ -383,105 +610,63 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260205.0': - resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260421.1': resolution: {integrity: sha512-DLU5ZTZ1VHeZZnj0PuVJEMHKGisfLe2XShyImP5P/PPj/m/t7CLEJmPiI7FMxvT7ynArkckJl7m+Z5x7u4Kkdw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260205.0': - resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260421.1': resolution: {integrity: sha512-Trotq3xRAkIcpC505WoxM8+kIH4JIvOJCNuRatyHcz9uF5S+ukgiVUFUlM+GIjw1uCM/Bda2St+vSniX1RZdpw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260205.0': - resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - '@cloudflare/workerd-linux-64@1.20260421.1': resolution: {integrity: sha512-938QjUv0z+QqK6BAvgwX/lCIZ2b224ZXoXtGTbhyNVMhB+mt4Dj24cj9qca4ekNXjVM7uTKp1yOHZO97fVSacw==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260205.0': - resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260421.1': resolution: {integrity: sha512-YI4+mLfwnJcKJ+iPyxzx+tp2Jy4o29BxBPSQGZxl/AZyvZ9eTKsmNZmtjEiT4i3O/M0tdO/B/d9ESDHbRCs2rQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260205.0': - resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - '@cloudflare/workerd-windows-64@1.20260421.1': resolution: {integrity: sha512-q1SFgwlNH9lFmw74vh7EJbJtduo92Nx51mNOfd3/u6pux6AldcwRviYzKEEv3FEbtv6OBB7J8D5f8vtZj7Z6Sg==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20260205.0': - resolution: {integrity: sha512-LTnpvcodmiuMwxmbrO2Fd0+Avbm2UVLLJxT8J2pRWPfoM44gmbIecXwOPZmDAMeadKWrBsQ+B0sloQAhUu5fpA==} - - '@cloudflare/workers-types@4.20260422.1': - resolution: {integrity: sha512-kI4baXLJloST4J/PxAfhz3azy4TPC7L2wcX6nusOzSNJJD/L5CSsivSJobgkYaMG5yspzRFQUQlCdPza0nTHbQ==} + '@cloudflare/workers-types@4.20260424.1': + resolution: {integrity: sha512-0DLJ9yEk1KKzPbqop80Gw/P1wkKKzawmipULiJWdBXIBCoMvE0OVWms3IrL/Q/G7tfmPop9yF4XlZ69k9JLYng==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} - cpu: [arm64] - os: [android] + cpu: [ppc64] + os: [aix] '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} @@ -489,10 +674,10 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} - cpu: [arm] + cpu: [arm64] os: [android] '@esbuild/android-arm@0.27.3': @@ -501,10 +686,10 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm] os: [android] '@esbuild/android-x64@0.27.3': @@ -513,11 +698,11 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] + cpu: [x64] + os: [android] '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} @@ -525,10 +710,10 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.27.3': @@ -537,11 +722,11 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] + cpu: [x64] + os: [darwin] '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} @@ -549,10 +734,10 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.27.3': @@ -561,11 +746,11 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} - cpu: [arm64] - os: [linux] + cpu: [x64] + os: [freebsd] '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} @@ -573,10 +758,10 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} - cpu: [arm] + cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.27.3': @@ -585,10 +770,10 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.27.3': @@ -597,10 +782,10 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} - cpu: [loong64] + cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.27.3': @@ -609,10 +794,10 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} - cpu: [mips64el] + cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.27.3': @@ -621,10 +806,10 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} - cpu: [ppc64] + cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.27.3': @@ -633,10 +818,10 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} - cpu: [riscv64] + cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.27.3': @@ -645,10 +830,10 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} - cpu: [s390x] + cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.27.3': @@ -657,10 +842,10 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.27.3': @@ -669,11 +854,11 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] + cpu: [x64] + os: [linux] '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} @@ -681,10 +866,10 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.27.3': @@ -693,11 +878,11 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] + cpu: [x64] + os: [netbsd] '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} @@ -705,10 +890,10 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.27.3': @@ -717,11 +902,11 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] + cpu: [x64] + os: [openbsd] '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} @@ -729,11 +914,11 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} - cpu: [x64] - os: [sunos] + cpu: [arm64] + os: [openharmony] '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} @@ -741,11 +926,11 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} - cpu: [arm64] - os: [win32] + cpu: [x64] + os: [sunos] '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} @@ -753,10 +938,10 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.27.3': @@ -765,10 +950,10 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.27.3': @@ -777,6 +962,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -884,8 +1075,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} '@img/sharp-darwin-arm64@0.34.5': @@ -1057,8 +1248,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@lucide/svelte@1.8.0': - resolution: {integrity: sha512-+zYQUKqEOVP5lxbGmxL1OVgGMQtRK91eIJ0bR+3Cr1ts4oQEsQfxyzzd5X47psJlblAuGFrl2xm4YuATjR9oaA==} + '@lucide/svelte@1.9.0': + resolution: {integrity: sha512-+5RmY0tqeuGGbiI2BnyDp1Ft/EP7AKReStHeFEKJ0mPcW1w51Tm41R2GvUfz0sGLJhmRhVAe2zKaTfqrcpJqqg==} peerDependencies: svelte: ^5 @@ -1071,17 +1262,17 @@ packages: '@mary-ext/event-iterator@1.0.0': resolution: {integrity: sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==} - '@mary-ext/simple-event-emitter@1.0.0': - resolution: {integrity: sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg==} + '@mary-ext/simple-event-emitter@1.0.1': + resolution: {integrity: sha512-9+VvZisxZ/gSg+JJH7hmXaA8Qj42Qjz3O58RSB+INYc8iLA0icATZxHB9vKbj59ojDGZjO3hCKzMXocx3L0H8w==} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@noble/secp256k1@3.0.0': - resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} + '@noble/secp256k1@3.1.0': + resolution: {integrity: sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1100,16 +1291,16 @@ packages: peerDependencies: svelte: ^4 || ^5 - '@optique/core@0.6.11': - resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==} + '@optique/core@1.0.2': + resolution: {integrity: sha512-znsqMmjAdeOgSJzdJlpZpgAscojwQmeQYXzYnuEKllz5VCj6WyEkdzU4QuvJQtWQY3ve2taXwudEBRur0VHBOQ==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} - '@optique/run@0.6.11': - resolution: {integrity: sha512-tsXBEygGSzNpFK2gjsRlXBn7FiScUeLFWIZNpoAZ8iG85Km0/3K9xgqlQAXoQ+uEZBe4XplnzyCDvmEgbyNT8w==} + '@optique/run@1.0.2': + resolution: {integrity: sha512-0Wc+zC8SLGV8zXQX+pk+o0c6wE/ddx/36CHZ0toTh5lApsjruUuGhqbxvljerAAG5un1xQbOLxzksBVC6UPgSg==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1123,220 +1314,220 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} cpu: [x64] os: [win32] @@ -1344,8 +1535,8 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} - '@speed-highlight/core@1.2.14': - resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1361,8 +1552,8 @@ packages: '@sveltejs/kit': ^2.0.0 wrangler: ^4.0.0 - '@sveltejs/kit@2.57.1': - resolution: {integrity: sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==} + '@sveltejs/kit@2.58.0': + resolution: {integrity: sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1662,6 +1853,36 @@ packages: '@tiptap/core': 3.22.4 '@tiptap/pm': 3.22.4 + '@turbo/darwin-64@2.9.6': + resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.6': + resolution: {integrity: sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.6': + resolution: {integrity: sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.6': + resolution: {integrity: sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.6': + resolution: {integrity: sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.6': + resolution: {integrity: sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A==} + cpu: [arm64] + os: [win32] + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1689,8 +1910,8 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -1766,34 +1987,34 @@ packages: '@use-gesture/vanilla@10.3.1': resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==} - '@vitest/expect@4.1.0': - resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.1.0': - resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.1.0': - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.1.0': - resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.1.0': - resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} - '@vitest/spy@4.1.0': - resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.1.0': - resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1805,8 +2026,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -1973,8 +2194,8 @@ packages: emoji-picker-element@1.29.1: resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -1987,13 +2208,13 @@ packages: es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true @@ -2164,8 +2385,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2197,8 +2418,8 @@ packages: hls.js@1.6.16: resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} - hono@4.12.8: - resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + hono@4.12.15: + resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} engines: {node: '>=16.9.0'} human-id@4.1.3: @@ -2427,11 +2648,6 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true - miniflare@4.20260205.0: - resolution: {integrity: sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==} - engines: {node: '>=18.0.0'} - hasBin: true - miniflare@4.20260421.0: resolution: {integrity: sha512-7ZkNQ7brgQ2hh5ha9iQCDUjxBkLvuiG2VdDns9esRL8O8lXg+MoP6E0dO1rtp+ZY2I+vV1tPWr6td5IojkewLw==} engines: {node: '>=18.0.0'} @@ -2468,8 +2684,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.7: - resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} + nanoid@5.1.9: + resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==} engines: {node: ^18 || >=20} hasBin: true @@ -2527,8 +2743,13 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - partysocket@1.1.11: - resolution: {integrity: sha512-P0EtOQiAwvLriqLgdThcSaREfz3bP77LkLSdmXq680BosPKvGSoGTh/d0g3S+UNmaqcw89Ad7JXHHKyRx3xU9Q==} + partysocket@1.1.18: + resolution: {integrity: sha512-SyuvH9VavWOSa14v6dYdp3yfSUDII4BQB1+TkGOFBkjfZKjnDBiba4fhdhwBlqGBkqw4ea3gTA1DYhSffX24Wg==} + peerDependencies: + react: '>=17' + peerDependenciesMeta: + react: + optional: true path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -2589,10 +2810,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -2657,8 +2874,8 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -2687,8 +2904,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier-plugin-tailwindcss@0.7.2: - resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + prettier-plugin-tailwindcss@0.7.3: + resolution: {integrity: sha512-lckXaWWdo2ZVXoMoUO3WIBiz9hVY+YBEh1gYyMFfrWP9WZW/wpFXQKizHx7WrFQFMkcG0bGShdpp531X1n+qpg==} engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -2747,8 +2964,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -2824,13 +3041,13 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2927,8 +3144,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -2994,8 +3211,8 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte@5.55.4: - resolution: {integrity: sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==} + svelte@5.55.5: + resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} engines: {node: '>=18'} tabbable@6.4.0: @@ -3038,12 +3255,12 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -3098,6 +3315,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + turbo@2.9.6: + resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3126,12 +3347,8 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} - engines: {node: '>=20.18.1'} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} undici@7.24.8: resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} @@ -3164,8 +3381,8 @@ packages: typescript: optional: true - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3215,21 +3432,23 @@ packages: vite: optional: true - vitest@4.1.0: - resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.0 - '@vitest/browser-preview': 4.1.0 - '@vitest/browser-webdriverio': 4.1.0 - '@vitest/ui': 4.1.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3243,6 +3462,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -3284,11 +3507,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260205.0: - resolution: {integrity: sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==} - engines: {node: '>=16'} - hasBin: true - workerd@1.20260421.1: resolution: {integrity: sha512-zTYD+xFR4d7TUCxsyl7FTPth9a8CDgk8pM7xUWbJxo0SGUx+2e5C7Q5LrramBZwmuAErtzXmOjlQ15PtkPAhZA==} engines: {node: '>=16'} @@ -3298,16 +3516,6 @@ packages: resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} engines: {node: '>=12'} - wrangler@4.63.0: - resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} - engines: {node: '>=20.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260205.0 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - wrangler@4.84.1: resolution: {integrity: sha512-Xe1S/Bik7pNdtdJ+asHsEZC2dX9k3WxYn2BbxFtOrrLVxN/LKi750zsrjX41jSAk00M/O1l7jzyQV4sQqw8ftg==} engines: {node: '>=20.3.0'} @@ -3357,10 +3565,6 @@ packages: snapshots: - '@atcute/atproto@3.1.10': - dependencies: - '@atcute/lexicons': 1.2.9 - '@atcute/atproto@3.1.11': dependencies: '@atcute/lexicons': 1.3.0 @@ -3393,55 +3597,57 @@ snapshots: '@atcute/client@4.2.1': dependencies: '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@atcute/crypto@2.4.1': dependencies: '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 - '@noble/secp256k1': 3.0.0 + '@noble/secp256k1': 3.1.0 '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': dependencies: '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@atcute/util-fetch': 1.0.5 '@badrap/valita': 0.4.6 '@atcute/identity@1.1.4': dependencies: - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@badrap/valita': 0.4.6 '@atcute/jetstream@1.1.2': dependencies: - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@badrap/valita': 0.4.6 '@mary-ext/event-iterator': 1.0.0 - '@mary-ext/simple-event-emitter': 1.0.0 - partysocket: 1.1.11 + '@mary-ext/simple-event-emitter': 1.0.1 + partysocket: 1.1.18 type-fest: 4.41.0 yocto-queue: 1.2.2 + transitivePeerDependencies: + - react - '@atcute/lex-cli@2.5.3': + '@atcute/lex-cli@2.8.1': dependencies: '@atcute/identity': 1.1.4 '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicon-doc': 2.1.2 + '@atcute/lexicon-doc': 2.2.0 '@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4) - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@badrap/valita': 0.4.6 - '@optique/core': 0.6.11 - '@optique/run': 0.6.11 + '@optique/core': 1.0.2 + '@optique/run': 1.0.2 picocolors: 1.1.1 - prettier: 3.8.1 + prettier: 3.8.3 - '@atcute/lexicon-doc@2.1.2': + '@atcute/lexicon-doc@2.2.0': dependencies: '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.9 + '@atcute/lexicons': 1.3.0 '@atcute/uint8array': 1.1.1 - '@atcute/util-text': 1.1.1 + '@atcute/util-text': 1.3.1 '@badrap/valita': 0.4.6 '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4)': @@ -3449,23 +3655,16 @@ snapshots: '@atcute/crypto': 2.4.1 '@atcute/identity': 1.1.4 '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) - '@atcute/lexicon-doc': 2.1.2 + '@atcute/lexicon-doc': 2.2.0 '@atcute/lexicons': 1.3.0 - '@atcute/repo': 0.1.3 + '@atcute/repo': 0.1.4 '@atcute/util-fetch': 1.0.5 '@badrap/valita': 0.4.6 - '@atcute/lexicons@1.2.9': - dependencies: - '@atcute/uint8array': 1.1.1 - '@atcute/util-text': 1.1.1 - '@standard-schema/spec': 1.1.0 - esm-env: 1.2.2 - '@atcute/lexicons@1.3.0': dependencies: '@atcute/uint8array': 1.1.1 - '@atcute/util-text': 1.2.0 + '@atcute/util-text': 1.3.1 '@standard-schema/spec': 1.1.0 esm-env: 1.2.2 @@ -3484,7 +3683,7 @@ snapshots: '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 '@badrap/valita': 0.4.6 - nanoid: 5.1.7 + nanoid: 5.1.9 '@atcute/oauth-keyset@0.1.0': dependencies: @@ -3501,7 +3700,7 @@ snapshots: '@atcute/oauth-types': 0.1.1 '@atcute/util-fetch': 1.0.5 '@badrap/valita': 0.4.6 - nanoid: 5.1.7 + nanoid: 5.1.9 '@atcute/oauth-types@0.1.1': dependencies: @@ -3510,7 +3709,7 @@ snapshots: '@atcute/oauth-keyset': 0.1.0 '@badrap/valita': 0.4.6 - '@atcute/repo@0.1.3': + '@atcute/repo@0.1.4': dependencies: '@atcute/car': 5.1.1 '@atcute/cbor': 2.3.2 @@ -3532,11 +3731,7 @@ snapshots: dependencies: '@badrap/valita': 0.4.6 - '@atcute/util-text@1.1.1': - dependencies: - unicode-segmenter: 0.14.5 - - '@atcute/util-text@1.2.0': + '@atcute/util-text@1.3.1': dependencies: unicode-segmenter: 0.14.5 @@ -3552,15 +3747,15 @@ snapshots: '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 '@badrap/valita': 0.4.6 - nanoid: 5.1.7 + nanoid: 5.1.9 '@babel/runtime@7.29.2': {} '@badrap/valita@0.4.6': {} - '@changesets/apply-release-plan@7.1.0': + '@changesets/apply-release-plan@7.1.1': dependencies: - '@changesets/config': 3.1.3 + '@changesets/config': 3.1.4 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.4 '@changesets/should-skip-package': 0.1.2 @@ -3574,10 +3769,10 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.4 - '@changesets/assemble-release-plan@6.0.9': + '@changesets/assemble-release-plan@6.0.10': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-dependents-graph': 2.1.4 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -3587,15 +3782,15 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.30.0(@types/node@25.5.0)': + '@changesets/cli@2.31.0(@types/node@25.6.0)': dependencies: - '@changesets/apply-release-plan': 7.1.0 - '@changesets/assemble-release-plan': 6.0.9 + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.3 + '@changesets/config': 3.1.4 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.15 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 @@ -3603,7 +3798,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.5.0) + '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -3618,10 +3813,10 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@changesets/config@3.1.3': + '@changesets/config@3.1.4': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-dependents-graph': 2.1.4 '@changesets/logger': 0.1.1 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 @@ -3633,17 +3828,17 @@ snapshots: dependencies: extendable-error: 0.1.7 - '@changesets/get-dependents-graph@2.1.3': + '@changesets/get-dependents-graph@2.1.4': dependencies: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 semver: 7.7.4 - '@changesets/get-release-plan@4.0.15': + '@changesets/get-release-plan@4.0.16': dependencies: - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.3 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.7 '@changesets/types': 6.1.0 @@ -3703,68 +3898,40 @@ snapshots: '@cloudflare/kv-asset-handler@0.4.2': {} - '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260205.0 - '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260421.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: workerd: 1.20260421.1 - '@cloudflare/workerd-darwin-64@1.20260205.0': - optional: true - '@cloudflare/workerd-darwin-64@1.20260421.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260205.0': - optional: true - '@cloudflare/workerd-darwin-arm64@1.20260421.1': optional: true - '@cloudflare/workerd-linux-64@1.20260205.0': - optional: true - '@cloudflare/workerd-linux-64@1.20260421.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260205.0': - optional: true - '@cloudflare/workerd-linux-arm64@1.20260421.1': optional: true - '@cloudflare/workerd-windows-64@1.20260205.0': - optional: true - '@cloudflare/workerd-windows-64@1.20260421.1': optional: true - '@cloudflare/workers-types@4.20260205.0': {} - - '@cloudflare/workers-types@4.20260422.1': {} + '@cloudflare/workers-types@4.20260424.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.9.2': + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.2': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true @@ -3774,162 +3941,162 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.0': - optional: true - '@esbuild/aix-ppc64@0.27.3': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/aix-ppc64@0.27.7': optional: true '@esbuild/android-arm64@0.27.3': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm64@0.27.7': optional: true '@esbuild/android-arm@0.27.3': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-arm@0.27.7': optional: true '@esbuild/android-x64@0.27.3': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/android-x64@0.27.7': optional: true '@esbuild/darwin-arm64@0.27.3': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-arm64@0.27.7': optional: true '@esbuild/darwin-x64@0.27.3': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/darwin-x64@0.27.7': optional: true '@esbuild/freebsd-arm64@0.27.3': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-arm64@0.27.7': optional: true '@esbuild/freebsd-x64@0.27.3': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/freebsd-x64@0.27.7': optional: true '@esbuild/linux-arm64@0.27.3': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm64@0.27.7': optional: true '@esbuild/linux-arm@0.27.3': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-arm@0.27.7': optional: true '@esbuild/linux-ia32@0.27.3': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-ia32@0.27.7': optional: true '@esbuild/linux-loong64@0.27.3': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-loong64@0.27.7': optional: true '@esbuild/linux-mips64el@0.27.3': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-mips64el@0.27.7': optional: true '@esbuild/linux-ppc64@0.27.3': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-ppc64@0.27.7': optional: true '@esbuild/linux-riscv64@0.27.3': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-riscv64@0.27.7': optional: true '@esbuild/linux-s390x@0.27.3': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-s390x@0.27.7': optional: true '@esbuild/linux-x64@0.27.3': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/linux-x64@0.27.7': optional: true '@esbuild/netbsd-arm64@0.27.3': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-arm64@0.27.7': optional: true '@esbuild/netbsd-x64@0.27.3': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/netbsd-x64@0.27.7': optional: true '@esbuild/openbsd-arm64@0.27.3': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-arm64@0.27.7': optional: true '@esbuild/openbsd-x64@0.27.3': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openbsd-x64@0.27.7': optional: true '@esbuild/openharmony-arm64@0.27.3': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/openharmony-arm64@0.27.7': optional: true '@esbuild/sunos-x64@0.27.3': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/sunos-x64@0.27.7': optional: true '@esbuild/win32-arm64@0.27.3': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-arm64@0.27.7': optional: true '@esbuild/win32-ia32@0.27.3': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-ia32@0.27.7': optional: true '@esbuild/win32-x64@0.27.3': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))': dependencies: eslint: 10.2.1(jiti@2.6.1) @@ -3985,23 +4152,23 @@ snapshots: '@fontsource-variable/geist@5.2.8': {} - '@foxui/core@0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/core@0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: '@fontsource-variable/geist': 5.2.8 '@fontsource-variable/geist-mono': 5.2.7 - '@jis3r/icons': 2.7.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - '@lucide/svelte': 1.8.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + '@jis3r/icons': 2.7.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + '@lucide/svelte': 1.9.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + '@number-flow/svelte': 0.4.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) clsx: 2.1.1 dompurify: 3.4.1 - mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - svelte: 5.55.4(@typescript-eslint/types@8.59.0) - svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + mode-watcher: 1.1.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) + svelte-sonner: 1.1.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) tailwind-merge: 3.5.0 tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.4) tailwindcss: 4.2.4 - web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + web-haptics: 0.0.6(svelte@5.55.5(@typescript-eslint/types@8.59.0)) transitivePeerDependencies: - '@internationalized/date' - '@sveltejs/kit' @@ -4009,20 +4176,20 @@ snapshots: - react-dom - vue - '@foxui/social@0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/social@0.8.10(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: '@atcute/bluesky': 3.3.3 '@atcute/bluesky-richtext-segmenter': 3.0.0 - '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) - '@foxui/text': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) - '@foxui/time': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/text': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/time': 0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@use-gesture/vanilla': 10.3.1 - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) emoji-picker-element: 1.29.1 hls.js: 1.6.16 is-emoji-supported: 0.0.5 plyr: 3.8.4 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) tailwindcss: 4.2.4 transitivePeerDependencies: - '@internationalized/date' @@ -4034,10 +4201,10 @@ snapshots: - react-dom - vue - '@foxui/text@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/text@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(highlight.js@11.11.1)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: '@floating-ui/dom': 1.7.6 - '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-code-block-lowlight': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(highlight.js@11.11.1)(lowlight@3.3.0) @@ -4057,11 +4224,11 @@ snapshots: '@tiptap/pm': 3.22.4 '@tiptap/starter-kit': 3.22.4 '@tiptap/suggestion': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) lowlight: 3.3.0 state: link:@tiptap/pm/state - svelte: 5.55.4(@typescript-eslint/types@8.59.0) - svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) + svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.5(@typescript-eslint/types@8.59.0)) tailwindcss: 4.2.4 transitivePeerDependencies: - '@internationalized/date' @@ -4073,12 +4240,12 @@ snapshots: - react-dom - vue - '@foxui/time@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': + '@foxui/time@0.8.5(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4)': dependencies: - '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) - '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + '@foxui/core': 0.9.1(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(tailwindcss@4.2.4) + '@number-flow/svelte': 0.4.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + bits-ui: 2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) tailwindcss: 4.2.4 transitivePeerDependencies: - '@internationalized/date' @@ -4103,7 +4270,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.0.0': {} + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -4187,7 +4354,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.10.0 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -4199,20 +4366,20 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.5.0)': + '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@internationalized/date@3.12.1': dependencies: '@swc/helpers': 0.5.21 - '@jis3r/icons@2.7.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': + '@jis3r/icons@2.7.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))': dependencies: - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4238,9 +4405,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lucide/svelte@1.8.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': + '@lucide/svelte@1.9.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))': dependencies: - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) '@manypkg/find-root@1.1.0': dependencies: @@ -4262,16 +4429,16 @@ snapshots: dependencies: yocto-queue: 1.2.2 - '@mary-ext/simple-event-emitter@1.0.0': {} + '@mary-ext/simple-event-emitter@1.0.1': {} - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true - '@noble/secp256k1@3.0.0': {} + '@noble/secp256k1@3.1.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -4285,19 +4452,19 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@number-flow/svelte@0.4.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))': + '@number-flow/svelte@0.4.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))': dependencies: esm-env: 1.2.2 number-flow: 0.6.0 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - '@optique/core@0.6.11': {} + '@optique/core@1.0.2': {} - '@optique/run@0.6.11': + '@optique/run@1.0.2': dependencies: - '@optique/core': 0.6.11 + '@optique/core': 1.0.2 - '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.127.0': {} '@polka/url@1.0.0-next.29': {} @@ -4313,135 +4480,135 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} - '@rollup/rollup-android-arm-eabi@4.60.1': + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true - '@rollup/rollup-android-arm64@4.60.1': + '@rollup/rollup-android-arm64@4.60.2': optional: true - '@rollup/rollup-darwin-arm64@4.60.1': + '@rollup/rollup-darwin-arm64@4.60.2': optional: true - '@rollup/rollup-darwin-x64@4.60.1': + '@rollup/rollup-darwin-x64@4.60.2': optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': + '@rollup/rollup-freebsd-arm64@4.60.2': optional: true - '@rollup/rollup-freebsd-x64@4.60.1': + '@rollup/rollup-freebsd-x64@4.60.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': + '@rollup/rollup-linux-arm-musleabihf@4.60.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': + '@rollup/rollup-linux-arm64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': + '@rollup/rollup-linux-arm64-musl@4.60.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': + '@rollup/rollup-linux-loong64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': + '@rollup/rollup-linux-loong64-musl@4.60.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': + '@rollup/rollup-linux-ppc64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': + '@rollup/rollup-linux-ppc64-musl@4.60.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': + '@rollup/rollup-linux-riscv64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': + '@rollup/rollup-linux-riscv64-musl@4.60.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': + '@rollup/rollup-linux-s390x-gnu@4.60.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': + '@rollup/rollup-linux-x64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': + '@rollup/rollup-linux-x64-musl@4.60.2': optional: true - '@rollup/rollup-openbsd-x64@4.60.1': + '@rollup/rollup-openbsd-x64@4.60.2': optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': + '@rollup/rollup-openharmony-arm64@4.60.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.1': + '@rollup/rollup-win32-arm64-msvc@4.60.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.1': + '@rollup/rollup-win32-ia32-msvc@4.60.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.1': + '@rollup/rollup-win32-x64-gnu@4.60.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.1': + '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true '@sindresorhus/is@7.2.0': {} - '@speed-highlight/core@1.2.14': {} + '@speed-highlight/core@1.2.15': {} '@standard-schema/spec@1.1.0': {} @@ -4449,18 +4616,18 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1))': + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1))': dependencies: - '@cloudflare/workers-types': 4.20260422.1 - '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@cloudflare/workers-types': 4.20260424.1 + '@sveltejs/kit': 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) worktop: 0.8.0-next.18 - wrangler: 4.84.1(@cloudflare/workers-types@4.20260422.1) + wrangler: 4.84.1(@cloudflare/workers-types@4.20260424.1) - '@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -4471,19 +4638,19 @@ snapshots: mrmime: 2.0.1 set-cookie-parser: 3.1.0 sirv: 3.0.2 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) optionalDependencies: typescript: 6.0.3 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - vitefu: 1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) + vitefu: 1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) '@swc/helpers@0.5.21': dependencies: @@ -4497,7 +4664,7 @@ snapshots: '@tailwindcss/node@4.2.4': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 + enhanced-resolve: 5.21.0 jiti: 2.6.1 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -4555,12 +4722,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 - '@tailwindcss/vite@4.2.4(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': dependencies: '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 tailwindcss: 4.2.4 - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) '@tiptap/core@3.22.4(@tiptap/pm@3.22.4)': dependencies: @@ -4749,6 +4916,24 @@ snapshots: '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) '@tiptap/pm': 3.22.4 + '@turbo/darwin-64@2.9.6': + optional: true + + '@turbo/darwin-arm64@2.9.6': + optional: true + + '@turbo/linux-64@2.9.6': + optional: true + + '@turbo/linux-arm64@2.9.6': + optional: true + + '@turbo/windows-64@2.9.6': + optional: true + + '@turbo/windows-arm64@2.9.6': + optional: true + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -4775,13 +4960,13 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@25.5.0': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 '@types/pg@8.20.0': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -4858,7 +5043,7 @@ snapshots: debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -4886,44 +5071,44 @@ snapshots: dependencies: '@use-gesture/core': 10.3.1 - '@vitest/expect@4.1.0': + '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3))': dependencies: - '@vitest/spy': 4.1.0 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) - '@vitest/pretty-format@4.1.0': + '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.0': + '@vitest/runner@4.1.5': dependencies: - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - '@vitest/snapshot@4.1.0': + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.0': {} + '@vitest/spy@4.1.5': {} - '@vitest/utils@4.1.0': + '@vitest/utils@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.0 + '@vitest/pretty-format': 4.1.5 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -4933,7 +5118,7 @@ snapshots: acorn@8.16.0: {} - ajv@6.14.0: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -4966,15 +5151,15 @@ snapshots: dependencies: is-windows: 1.0.2 - bits-ui@2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + bits-ui@2.18.0(@internationalized/date@3.12.1)(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: '@floating-ui/core': 1.7.5 '@floating-ui/dom': 1.7.6 '@internationalized/date': 3.12.1 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - svelte: 5.55.4(@typescript-eslint/types@8.59.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + runed: 0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -4989,9 +5174,9 @@ snapshots: dependencies: fill-range: 7.1.1 - bundle-require@5.1.0(esbuild@0.27.0): + bundle-require@5.1.0(esbuild@0.27.7): dependencies: - esbuild: 0.27.0 + esbuild: 0.27.7 load-tsconfig: 0.2.5 cac@6.7.14: {} @@ -5060,7 +5245,7 @@ snapshots: emoji-picker-element@1.29.1: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.21.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -5074,35 +5259,6 @@ snapshots: es-module-lexer@2.0.0: {} - esbuild@0.27.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 - esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -5132,13 +5288,42 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.6.1)): dependencies: eslint: 10.2.1(jiti@2.6.1) - eslint-plugin-svelte@3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + eslint-plugin-svelte@3.17.1(eslint@10.2.1(jiti@2.6.1))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -5146,13 +5331,13 @@ snapshots: esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 - postcss: 8.5.8 - postcss-load-config: 3.1.4(postcss@8.5.8) - postcss-safe-parser: 7.0.1(postcss@8.5.8) + postcss: 8.5.10 + postcss-load-config: 3.1.4(postcss@8.5.10) + postcss-safe-parser: 7.0.1(postcss@8.5.10) semver: 7.7.4 - svelte-eslint-parser: 1.6.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + svelte-eslint-parser: 1.6.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) optionalDependencies: - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) transitivePeerDependencies: - ts-node @@ -5186,7 +5371,7 @@ snapshots: '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.14.0 + ajv: 6.15.0 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 @@ -5299,7 +5484,7 @@ snapshots: dependencies: magic-string: 0.30.21 mlly: 1.8.2 - rollup: 4.60.1 + rollup: 4.60.2 flat-cache@4.0.1: dependencies: @@ -5323,7 +5508,7 @@ snapshots: fsevents@2.3.3: optional: true - get-tsconfig@4.13.6: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -5354,7 +5539,7 @@ snapshots: hls.js@1.6.16: {} - hono@4.12.8: {} + hono@4.12.15: {} human-id@4.1.3: {} @@ -5524,18 +5709,6 @@ snapshots: mini-svg-data-uri@1.4.4: {} - miniflare@4.20260205.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.18.2 - workerd: 1.20260205.0 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - miniflare@4.20260421.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -5559,11 +5732,11 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mode-watcher@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + mode-watcher@1.1.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: - runed: 0.25.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - svelte: 5.55.4(@typescript-eslint/types@8.59.0) - svelte-toolbelt: 0.7.1(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + runed: 0.25.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) + svelte-toolbelt: 0.7.1(svelte@5.55.5(@typescript-eslint/types@8.59.0)) mri@1.2.0: {} @@ -5579,7 +5752,7 @@ snapshots: nanoid@3.3.11: {} - nanoid@5.1.7: {} + nanoid@5.1.9: {} natural-compare@1.4.0: {} @@ -5632,7 +5805,7 @@ snapshots: dependencies: quansync: 0.2.11 - partysocket@1.1.11: + partysocket@1.1.18: dependencies: event-target-polyfill: 0.0.4 @@ -5685,8 +5858,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.3: {} - picomatch@4.0.4: {} pify@4.0.1: {} @@ -5707,35 +5878,36 @@ snapshots: rangetouch: 2.0.1 url-polyfill: 1.1.14 - postcss-load-config@3.1.4(postcss@8.5.8): + postcss-load-config@3.1.4(postcss@8.5.10): dependencies: lilconfig: 2.1.0 yaml: 1.10.3 optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@1.10.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 - postcss: 8.5.8 + postcss: 8.5.10 tsx: 4.21.0 + yaml: 1.10.3 - postcss-safe-parser@7.0.1(postcss@8.5.8): + postcss-safe-parser@7.0.1(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 - postcss-scss@4.0.9(postcss@8.5.8): + postcss-scss@4.0.9(postcss@8.5.10): dependencies: - postcss: 8.5.8 + postcss: 8.5.10 postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.8: + postcss@8.5.10: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -5753,20 +5925,20 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + prettier-plugin-svelte@3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: - prettier: 3.8.1 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + prettier: 3.8.3 + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)))(prettier@3.8.1): + prettier-plugin-tailwindcss@0.7.3(prettier-plugin-svelte@3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)))(prettier@3.8.3): dependencies: - prettier: 3.8.1 + prettier: 3.8.3 optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + prettier-plugin-svelte: 3.5.1(prettier@3.8.3)(svelte@5.55.5(@typescript-eslint/types@8.59.0)) prettier@2.8.8: {} - prettier@3.8.1: {} + prettier@3.8.3: {} prosemirror-changeset@2.4.1: dependencies: @@ -5862,56 +6034,56 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.15: + rolldown@1.0.0-rc.17: dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - - rollup@4.60.1: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 rope-sequence@1.3.4: {} @@ -5920,29 +6092,29 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.23.4(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + runed@0.23.4(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: esm-env: 1.2.2 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - runed@0.25.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + runed@0.25.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: esm-env: 1.2.2 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - runed@0.28.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + runed@0.28.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: esm-env: 1.2.2 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - runed@0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + runed@0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) optionalDependencies: - '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/kit': 2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) sade@1.8.1: dependencies: @@ -5956,7 +6128,7 @@ snapshots: sharp@0.34.5: dependencies: - '@img/colour': 1.0.0 + '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.7.4 optionalDependencies: @@ -6018,7 +6190,7 @@ snapshots: stackback@0.0.2: {} - std-env@4.0.0: {} + std-env@4.1.0: {} strip-ansi@6.0.1: dependencies: @@ -6037,66 +6209,66 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 supports-color@10.2.2: {} - svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3): + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.4) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) typescript: 6.0.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.6.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + svelte-eslint-parser@1.6.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.8 - postcss-scss: 4.0.9(postcss@8.5.8) + postcss: 8.5.10 + postcss-scss: 4.0.9(postcss@8.5.10) postcss-selector-parser: 7.1.1 semver: 7.7.4 optionalDependencies: - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - svelte-sonner@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + svelte-sonner@1.1.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: - runed: 0.28.0(svelte@5.55.4(@typescript-eslint/types@8.59.0)) - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + runed: 0.28.0(svelte@5.55.5(@typescript-eslint/types@8.59.0)) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: '@floating-ui/dom': 1.7.6 '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/extension-floating-menu': 3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) '@tiptap/pm': 3.22.4 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - svelte-toolbelt@0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.59.0))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + runed: 0.35.1(@sveltejs/kit@2.58.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.59.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0))(typescript@6.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)))(svelte@5.55.5(@typescript-eslint/types@8.59.0)) style-to-object: 1.0.14 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte-toolbelt@0.7.1(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + svelte-toolbelt@0.7.1(svelte@5.55.5(@typescript-eslint/types@8.59.0)): dependencies: clsx: 2.1.1 - runed: 0.23.4(svelte@5.55.4(@typescript-eslint/types@8.59.0)) + runed: 0.23.4(svelte@5.55.5(@typescript-eslint/types@8.59.0)) style-to-object: 1.0.14 - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) - svelte@5.55.4(@typescript-eslint/types@8.59.0): + svelte@5.55.5(@typescript-eslint/types@8.59.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -6145,9 +6317,9 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.0.4: {} + tinyexec@1.1.1: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -6170,27 +6342,27 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@1.10.3): dependencies: - bundle-require: 5.1.0(esbuild@0.27.0) + bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 debug: 4.4.3 - esbuild: 0.27.0 + esbuild: 0.27.7 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.10)(tsx@4.21.0)(yaml@1.10.3) resolve-from: 5.0.0 - rollup: 4.60.1 + rollup: 4.60.2 source-map: 0.7.6 sucrase: 3.35.1 tinyexec: 0.3.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.10 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -6200,11 +6372,20 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.0 - get-tsconfig: 4.13.6 + esbuild: 0.27.7 + get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 + turbo@2.9.6: + optionalDependencies: + '@turbo/darwin-64': 2.9.6 + '@turbo/darwin-arm64': 2.9.6 + '@turbo/linux-64': 2.9.6 + '@turbo/linux-arm64': 2.9.6 + '@turbo/windows-64': 2.9.6 + '@turbo/windows-arm64': 2.9.6 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6228,9 +6409,7 @@ snapshots: ufo@1.6.3: {} - undici-types@7.18.2: {} - - undici@7.18.2: {} + undici-types@7.19.2: {} undici@7.24.8: {} @@ -6254,70 +6433,57 @@ snapshots: optionalDependencies: typescript: 6.0.3 - vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0): + vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 + postcss: 8.5.10 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.0 - fsevents: 2.3.3 - jiti: 2.6.1 - tsx: 4.21.0 - - vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.8 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.3 + '@types/node': 25.6.0 + esbuild: 0.27.7 fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 + yaml: 1.10.3 - vitefu@1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): + vitefu@1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)): optionalDependencies: - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) - vitest@4.1.0(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)): + vitest@4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)): dependencies: - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.0)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@1.10.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 transitivePeerDependencies: - msw w3c-keyname@2.2.8: {} - web-haptics@0.0.6(svelte@5.55.4(@typescript-eslint/types@8.59.0)): + web-haptics@0.0.6(svelte@5.55.5(@typescript-eslint/types@8.59.0)): optionalDependencies: - svelte: 5.55.4(@typescript-eslint/types@8.59.0) + svelte: 5.55.5(@typescript-eslint/types@8.59.0) which@2.0.2: dependencies: @@ -6330,14 +6496,6 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260205.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260205.0 - '@cloudflare/workerd-darwin-arm64': 1.20260205.0 - '@cloudflare/workerd-linux-64': 1.20260205.0 - '@cloudflare/workerd-linux-arm64': 1.20260205.0 - '@cloudflare/workerd-windows-64': 1.20260205.0 - workerd@1.20260421.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260421.1 @@ -6351,24 +6509,7 @@ snapshots: mrmime: 2.0.1 regexparam: 3.0.0 - wrangler@4.63.0(@cloudflare/workers-types@4.20260205.0): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0) - blake3-wasm: 2.1.5 - esbuild: 0.27.0 - miniflare: 4.20260205.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260205.0 - optionalDependencies: - '@cloudflare/workers-types': 4.20260205.0 - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - wrangler@4.84.1(@cloudflare/workers-types@4.20260422.1): + wrangler@4.84.1(@cloudflare/workers-types@4.20260424.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260421.1) @@ -6379,7 +6520,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260421.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260422.1 + '@cloudflare/workers-types': 4.20260424.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -6404,7 +6545,7 @@ snapshots: dependencies: '@poppinss/colors': 4.1.6 '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.14 + '@speed-highlight/core': 1.2.15 cookie: 1.1.1 youch-core: 0.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b92d403..ad4305c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,10 @@ packages: - - "examples/sveltekit-group-chat" + - "packages/*" + - "apps/*" -ignoredBuiltDependencies: +onlyBuiltDependencies: - esbuild - sharp - workerd + - "@tailwindcss/oxide" + - "@sveltejs/kit" diff --git a/tsconfig.build.json b/tsconfig.base.json similarity index 72% rename from tsconfig.build.json rename to tsconfig.base.json index 3e4282c..b989f00 100644 --- a/tsconfig.build.json +++ b/tsconfig.base.json @@ -4,13 +4,12 @@ "module": "ES2022", "moduleResolution": "bundler", "lib": ["ES2022"], - "types": ["@cloudflare/workers-types", "node"], "strict": true, "noEmit": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "isolatedModules": true - }, - "include": ["src"] + "isolatedModules": true, + "esModuleInterop": true + } } diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 2c00c47..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "lib": ["ES2022"], - "types": ["@cloudflare/workers-types"], - "strict": true, - "noEmit": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true - }, - "include": ["src", "app"], - "exclude": ["src/adapters/sqlite.ts", "src/generate.ts"] -} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..94b9292 --- /dev/null +++ b/turbo.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "stream", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "typecheck": { + "dependsOn": ["^build"] + }, + "test": { + "dependsOn": ["^build"], + "outputs": [] + }, + "dev": { + "cache": false, + "persistent": true, + "dependsOn": ["^build"] + }, + "clean": { + "cache": false + } + } +} -- 2.51.2 From 97bd494c4948c766d7db0c0269edbf5dffc87a86 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:37:35 +0200 Subject: [PATCH 16/18] add changeset, publish packages --- .changeset/frank-clouds-divide.md | 7 +++++++ packages/contrail/package.json | 3 +++ packages/lexicons/package.json | 3 +++ packages/sync/package.json | 3 +++ 4 files changed, 16 insertions(+) create mode 100644 .changeset/frank-clouds-divide.md diff --git a/.changeset/frank-clouds-divide.md b/.changeset/frank-clouds-divide.md new file mode 100644 index 0000000..01f171f --- /dev/null +++ b/.changeset/frank-clouds-divide.md @@ -0,0 +1,7 @@ +--- +"@atmo-dev/contrail": minor +"@atmo-dev/contrail-lexicons": minor +"@atmo-dev/contrail-sync": minor +--- + +split packages, monorepo diff --git a/packages/contrail/package.json b/packages/contrail/package.json index 5fec7a6..23c18c0 100644 --- a/packages/contrail/package.json +++ b/packages/contrail/package.json @@ -7,6 +7,9 @@ "files": [ "dist" ], + "publishConfig": { + "access": "public" + }, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/lexicons/package.json b/packages/lexicons/package.json index 2225f69..31d0e8f 100644 --- a/packages/lexicons/package.json +++ b/packages/lexicons/package.json @@ -7,6 +7,9 @@ "dist", "lexicon-templates/**/*.json" ], + "publishConfig": { + "access": "public" + }, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/sync/package.json b/packages/sync/package.json index f952514..52dc941 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -7,6 +7,9 @@ "files": [ "dist" ], + "publishConfig": { + "access": "public" + }, "exports": { ".": { "types": "./dist/index.d.ts", -- 2.51.2 From 4d11da3444d0db955f2c85222afbe3a6dbdc3175 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:43:29 +0200 Subject: [PATCH 17/18] fix build --- .changeset/config.json | 6 +++++- apps/group-chat/package.json | 2 +- apps/sveltekit-cloudflare-workers/package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 9166825..237216e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,9 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [], + "privatePackages": { + "version": false, + "tag": false + } } diff --git a/apps/group-chat/package.json b/apps/group-chat/package.json index 71d4219..54b950a 100644 --- a/apps/group-chat/package.json +++ b/apps/group-chat/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build && tsx scripts/append-scheduled.ts", + "build": "tsx scripts/generate.ts && lex-cli generate && vite build && tsx scripts/append-scheduled.ts", "generate": "tsx scripts/generate.ts", "generate:pull": "tsx scripts/generate.ts && lex-cli pull && tsx scripts/generate.ts && lex-cli pull && lex-cli generate", "sync": "tsx scripts/sync.ts", diff --git a/apps/sveltekit-cloudflare-workers/package.json b/apps/sveltekit-cloudflare-workers/package.json index 9c97ee5..939aba9 100644 --- a/apps/sveltekit-cloudflare-workers/package.json +++ b/apps/sveltekit-cloudflare-workers/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build && tsx scripts/append-scheduled.ts", + "build": "tsx scripts/generate.ts && lex-cli generate && vite build && tsx scripts/append-scheduled.ts", "generate": "tsx scripts/generate.ts", "generate:pull": "tsx scripts/generate.ts && lex-cli pull && tsx scripts/generate.ts && lex-cli pull && lex-cli generate", "sync": "tsx scripts/sync.ts", -- 2.51.2 From 4eaa77c624c7612da854eb0779a47c1f8b792d3e Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:45:21 +0200 Subject: [PATCH 18/18] dont build apps/ --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1abdd07..bc8d3b8 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,13 @@ "version": "0.0.0", "type": "module", "scripts": { - "build": "turbo run build", + "build": "turbo run build --filter=./packages/*", + "build:apps": "turbo run build", "test": "turbo run test", "typecheck": "turbo run typecheck", "dev": "turbo run dev", "clean": "turbo run clean", - "release": "turbo run build && changeset publish" + "release": "turbo run build --filter=./packages/* && changeset publish" }, "devDependencies": { "@changesets/cli": "^2.29.4",