@braintwopoint0/playback-commons 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,3 @@
1
- "use client";
2
-
3
1
  // src/playerdata/config.ts
4
2
  var PLAYERDATA_BASE_URL = "https://app.playerdata.co.uk";
5
3
  var PLAYERDATA_AUTH_URL = `${PLAYERDATA_BASE_URL}/api/oauth/authorize`;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/playerdata/config.ts","../../src/playerdata/auth.ts","../../src/playerdata/queries.ts","../../src/playerdata/client.ts"],"sourcesContent":["// PlayerData API endpoint constants\nexport const PLAYERDATA_BASE_URL = 'https://app.playerdata.co.uk'\nexport const PLAYERDATA_AUTH_URL = `${PLAYERDATA_BASE_URL}/api/oauth/authorize`\nexport const PLAYERDATA_TOKEN_URL = `${PLAYERDATA_BASE_URL}/api/oauth/token`\nexport const PLAYERDATA_GRAPHQL_URL = `${PLAYERDATA_BASE_URL}/api/graphql`\n","import { PLAYERDATA_AUTH_URL, PLAYERDATA_TOKEN_URL } from './config'\nimport type {\n PlayerDataCredentials,\n PlayerDataToken,\n PlayerDataTokenResponse,\n} from './types'\n\nfunction parseTokenResponse(raw: PlayerDataTokenResponse): PlayerDataToken {\n return {\n accessToken: raw.access_token,\n refreshToken: raw.refresh_token,\n expiresAt: Date.now() + raw.expires_in * 1000,\n }\n}\n\n/**\n * Generate the OAuth authorization URL for user-level access.\n * Redirect the user's browser to this URL to start the OAuth flow.\n */\nexport function generateAuthUrl(\n credentials: PlayerDataCredentials,\n redirectUri: string,\n state?: string\n): string {\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: credentials.clientId,\n redirect_uri: redirectUri,\n scope: '',\n })\n if (state) params.set('state', state)\n return `${PLAYERDATA_AUTH_URL}?${params.toString()}`\n}\n\n/**\n * Exchange an authorization code for access + refresh tokens.\n * Used after the user is redirected back from PlayerData.\n */\nexport async function exchangeCode(\n credentials: PlayerDataCredentials,\n code: string,\n redirectUri: string\n): Promise<PlayerDataToken> {\n const res = await fetch(PLAYERDATA_TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n code,\n redirect_uri: redirectUri,\n }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData code exchange failed (${res.status}): ${text}`)\n }\n\n const raw: PlayerDataTokenResponse = await res.json()\n return parseTokenResponse(raw)\n}\n\n/**\n * Refresh an expired access token using a refresh token.\n * IMPORTANT: PlayerData refresh tokens are single-use.\n * The response contains a NEW refresh token that must replace the old one.\n */\nexport async function refreshToken(\n credentials: PlayerDataCredentials,\n currentRefreshToken: string\n): Promise<PlayerDataToken> {\n const res = await fetch(PLAYERDATA_TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n refresh_token: currentRefreshToken,\n }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData token refresh failed (${res.status}): ${text}`)\n }\n\n const raw: PlayerDataTokenResponse = await res.json()\n return parseTokenResponse(raw)\n}\n\n/**\n * Get a service-level token using client credentials grant.\n * Used for org-wide data access without user interaction.\n */\nexport async function getServiceToken(\n credentials: PlayerDataCredentials\n): Promise<PlayerDataToken> {\n const res = await fetch(PLAYERDATA_TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData service token failed (${res.status}): ${text}`)\n }\n\n const raw: PlayerDataTokenResponse = await res.json()\n return parseTokenResponse(raw)\n}\n","// GraphQL queries for the PlayerData API\n// Uses configuredMetrics/configuredAggMetrics (not deprecated individual fields)\n\n/** Verify auth and get the current user's identity */\nexport const GET_CURRENT_PERSON = `\n query GetCurrentPerson {\n currentPerson {\n id\n name\n email\n athletes {\n id\n name\n club {\n id\n name\n }\n }\n clubs {\n id\n name\n }\n }\n }\n`\n\n/** Get club info and its athlete roster */\nexport const GET_CLUB = `\n query GetClub($clubId: ID!) {\n club(id: $clubId) {\n id\n name\n sportDefinition {\n id\n name\n positionDefinitions {\n id\n name\n initial\n }\n }\n athletes {\n id\n name\n initials\n customId\n topSpeedKph\n autoTopSpeedKph\n defaultPosition {\n id\n name\n initial\n }\n profilePicture {\n thumbnailUrl\n }\n }\n }\n }\n`\n\n/** List sessions for a club with optional date filters */\nexport const GET_CLUB_SESSIONS = `\n query GetClubSessions($clubId: ID!, $startTimeGteq: ISO8601DateTime, $startTimeLteq: ISO8601DateTime, $limit: Int, $offset: Int!) {\n club(id: $clubId) {\n id\n sessions(\n filter: { startTimeGteq: $startTimeGteq, startTimeLteq: $startTimeLteq }\n limit: $limit\n offset: $offset\n ) {\n id\n startTime\n endTime\n description\n ... on TrainingSession {\n __typename\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n }\n }\n }\n }\n`\n\n/** Get an athlete's sessions */\nexport const GET_ATHLETE_SESSIONS = `\n query GetAthleteSessions($athleteId: ID!, $limit: Int, $offset: Int) {\n athlete(id: $athleteId) {\n id\n name\n sessions(limit: $limit, offset: $offset) {\n id\n startTime\n endTime\n description\n ... on TrainingSession {\n __typename\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n }\n }\n }\n }\n`\n\n/** Get a session with per-athlete metrics via participations */\nexport const GET_SESSION_DETAILS = `\n query GetSessionDetails($sessionId: ID!) {\n session(id: $sessionId) {\n id\n startTime\n endTime\n description\n club {\n id\n name\n }\n ... on TrainingSession {\n __typename\n sessionParticipations {\n id\n athlete {\n id\n name\n initials\n }\n configuredMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n sessionParticipations {\n id\n athlete {\n id\n name\n initials\n }\n configuredMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n }\n }\n`\n\n/** Get aggregated metrics for an athlete over a date range */\nexport const GET_ATHLETE_SUMMARY = `\n query GetAthleteSummary($athleteId: ID!, $startTime: ISO8601DateTime!, $endTime: ISO8601DateTime) {\n athlete(id: $athleteId) {\n id\n name\n timeSafeMetricsSummary(startTime: $startTime, endTime: $endTime) {\n athlete {\n id\n name\n }\n configuredAggMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n }\n`\n","import { PLAYERDATA_GRAPHQL_URL } from './config'\nimport type { GraphQLResponse } from './types'\n\n/**\n * Execute a GraphQL query against the PlayerData API.\n * Stateless — caller provides the access token.\n */\nexport async function executeQuery<T = unknown>(\n token: string,\n query: string,\n variables?: Record<string, unknown>\n): Promise<T> {\n const res = await fetch(PLAYERDATA_GRAPHQL_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ query, variables }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData GraphQL request failed (${res.status}): ${text}`)\n }\n\n const json: GraphQLResponse<T> = await res.json()\n\n if (json.errors?.length) {\n const messages = json.errors.map((e) => e.message).join('; ')\n throw new Error(`PlayerData GraphQL errors: ${messages}`)\n }\n\n if (!json.data) {\n throw new Error('PlayerData GraphQL response missing data')\n }\n\n return json.data\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/playerdata/config.ts","../../src/playerdata/auth.ts","../../src/playerdata/queries.ts","../../src/playerdata/client.ts"],"sourcesContent":["// PlayerData API endpoint constants\nexport const PLAYERDATA_BASE_URL = 'https://app.playerdata.co.uk'\nexport const PLAYERDATA_AUTH_URL = `${PLAYERDATA_BASE_URL}/api/oauth/authorize`\nexport const PLAYERDATA_TOKEN_URL = `${PLAYERDATA_BASE_URL}/api/oauth/token`\nexport const PLAYERDATA_GRAPHQL_URL = `${PLAYERDATA_BASE_URL}/api/graphql`\n","import { PLAYERDATA_AUTH_URL, PLAYERDATA_TOKEN_URL } from './config'\nimport type {\n PlayerDataCredentials,\n PlayerDataToken,\n PlayerDataTokenResponse,\n} from './types'\n\nfunction parseTokenResponse(raw: PlayerDataTokenResponse): PlayerDataToken {\n return {\n accessToken: raw.access_token,\n refreshToken: raw.refresh_token,\n expiresAt: Date.now() + raw.expires_in * 1000,\n }\n}\n\n/**\n * Generate the OAuth authorization URL for user-level access.\n * Redirect the user's browser to this URL to start the OAuth flow.\n */\nexport function generateAuthUrl(\n credentials: PlayerDataCredentials,\n redirectUri: string,\n state?: string\n): string {\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: credentials.clientId,\n redirect_uri: redirectUri,\n scope: '',\n })\n if (state) params.set('state', state)\n return `${PLAYERDATA_AUTH_URL}?${params.toString()}`\n}\n\n/**\n * Exchange an authorization code for access + refresh tokens.\n * Used after the user is redirected back from PlayerData.\n */\nexport async function exchangeCode(\n credentials: PlayerDataCredentials,\n code: string,\n redirectUri: string\n): Promise<PlayerDataToken> {\n const res = await fetch(PLAYERDATA_TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n code,\n redirect_uri: redirectUri,\n }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData code exchange failed (${res.status}): ${text}`)\n }\n\n const raw: PlayerDataTokenResponse = await res.json()\n return parseTokenResponse(raw)\n}\n\n/**\n * Refresh an expired access token using a refresh token.\n * IMPORTANT: PlayerData refresh tokens are single-use.\n * The response contains a NEW refresh token that must replace the old one.\n */\nexport async function refreshToken(\n credentials: PlayerDataCredentials,\n currentRefreshToken: string\n): Promise<PlayerDataToken> {\n const res = await fetch(PLAYERDATA_TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n refresh_token: currentRefreshToken,\n }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData token refresh failed (${res.status}): ${text}`)\n }\n\n const raw: PlayerDataTokenResponse = await res.json()\n return parseTokenResponse(raw)\n}\n\n/**\n * Get a service-level token using client credentials grant.\n * Used for org-wide data access without user interaction.\n */\nexport async function getServiceToken(\n credentials: PlayerDataCredentials\n): Promise<PlayerDataToken> {\n const res = await fetch(PLAYERDATA_TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData service token failed (${res.status}): ${text}`)\n }\n\n const raw: PlayerDataTokenResponse = await res.json()\n return parseTokenResponse(raw)\n}\n","// GraphQL queries for the PlayerData API\n// Uses configuredMetrics/configuredAggMetrics (not deprecated individual fields)\n\n/** Verify auth and get the current user's identity */\nexport const GET_CURRENT_PERSON = `\n query GetCurrentPerson {\n currentPerson {\n id\n name\n email\n athletes {\n id\n name\n club {\n id\n name\n }\n }\n clubs {\n id\n name\n }\n }\n }\n`\n\n/** Get club info and its athlete roster */\nexport const GET_CLUB = `\n query GetClub($clubId: ID!) {\n club(id: $clubId) {\n id\n name\n sportDefinition {\n id\n name\n positionDefinitions {\n id\n name\n initial\n }\n }\n athletes {\n id\n name\n initials\n customId\n topSpeedKph\n autoTopSpeedKph\n defaultPosition {\n id\n name\n initial\n }\n profilePicture {\n thumbnailUrl\n }\n }\n }\n }\n`\n\n/** List sessions for a club with optional date filters */\nexport const GET_CLUB_SESSIONS = `\n query GetClubSessions($clubId: ID!, $startTimeGteq: ISO8601DateTime, $startTimeLteq: ISO8601DateTime, $limit: Int, $offset: Int!) {\n club(id: $clubId) {\n id\n sessions(\n filter: { startTimeGteq: $startTimeGteq, startTimeLteq: $startTimeLteq }\n limit: $limit\n offset: $offset\n ) {\n id\n startTime\n endTime\n description\n ... on TrainingSession {\n __typename\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n }\n }\n }\n }\n`\n\n/** Get an athlete's sessions */\nexport const GET_ATHLETE_SESSIONS = `\n query GetAthleteSessions($athleteId: ID!, $limit: Int, $offset: Int) {\n athlete(id: $athleteId) {\n id\n name\n sessions(limit: $limit, offset: $offset) {\n id\n startTime\n endTime\n description\n ... on TrainingSession {\n __typename\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n }\n }\n }\n }\n`\n\n/** Get a session with per-athlete metrics via participations */\nexport const GET_SESSION_DETAILS = `\n query GetSessionDetails($sessionId: ID!) {\n session(id: $sessionId) {\n id\n startTime\n endTime\n description\n club {\n id\n name\n }\n ... on TrainingSession {\n __typename\n sessionParticipations {\n id\n athlete {\n id\n name\n initials\n }\n configuredMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n sessionParticipations {\n id\n athlete {\n id\n name\n initials\n }\n configuredMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n }\n }\n`\n\n/** Get aggregated metrics for an athlete over a date range */\nexport const GET_ATHLETE_SUMMARY = `\n query GetAthleteSummary($athleteId: ID!, $startTime: ISO8601DateTime!, $endTime: ISO8601DateTime) {\n athlete(id: $athleteId) {\n id\n name\n timeSafeMetricsSummary(startTime: $startTime, endTime: $endTime) {\n athlete {\n id\n name\n }\n configuredAggMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n }\n`\n","import { PLAYERDATA_GRAPHQL_URL } from './config'\nimport type { GraphQLResponse } from './types'\n\n/**\n * Execute a GraphQL query against the PlayerData API.\n * Stateless — caller provides the access token.\n */\nexport async function executeQuery<T = unknown>(\n token: string,\n query: string,\n variables?: Record<string, unknown>\n): Promise<T> {\n const res = await fetch(PLAYERDATA_GRAPHQL_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ query, variables }),\n })\n\n if (!res.ok) {\n const text = await res.text()\n throw new Error(`PlayerData GraphQL request failed (${res.status}): ${text}`)\n }\n\n const json: GraphQLResponse<T> = await res.json()\n\n if (json.errors?.length) {\n const messages = json.errors.map((e) => e.message).join('; ')\n throw new Error(`PlayerData GraphQL errors: ${messages}`)\n }\n\n if (!json.data) {\n throw new Error('PlayerData GraphQL response missing data')\n }\n\n return json.data\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,28 @@
1
+ // src/security/index.ts
2
+ import { timingSafeEqual } from "crypto";
3
+
4
+ // src/utils/sanitize.ts
5
+ function sanitizeRedirect(raw) {
6
+ if (!raw) return "/";
7
+ if (raw.startsWith("/") && !raw.startsWith("//") && !raw.includes("@") && !raw.includes("\\")) {
8
+ return raw;
9
+ }
10
+ return "/";
11
+ }
12
+ function escapeHtml(s) {
13
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14
+ }
15
+
16
+ // src/security/index.ts
17
+ function verifyApiKey(request, expectedKey, headerName = "x-api-key") {
18
+ const apiKey = request.headers.get(headerName);
19
+ if (!apiKey || !expectedKey) return false;
20
+ if (apiKey.length !== expectedKey.length) return false;
21
+ return timingSafeEqual(Buffer.from(apiKey), Buffer.from(expectedKey));
22
+ }
23
+ export {
24
+ escapeHtml,
25
+ sanitizeRedirect,
26
+ verifyApiKey
27
+ };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/security/index.ts","../../src/utils/sanitize.ts"],"sourcesContent":["import { timingSafeEqual } from 'crypto'\n\n// Re-export client-safe utilities from utils\nexport { sanitizeRedirect, escapeHtml } from '../utils/sanitize'\n\n/**\n * Verify an API key from a request header using timing-safe comparison.\n * Prevents timing side-channel attacks on key verification.\n *\n * @param request - The incoming request\n * @param expectedKey - The expected API key value\n * @param headerName - The header to read (default: 'x-api-key')\n */\nexport function verifyApiKey(\n request: Request,\n expectedKey: string,\n headerName: string = 'x-api-key'\n): boolean {\n const apiKey = request.headers.get(headerName)\n if (!apiKey || !expectedKey) return false\n if (apiKey.length !== expectedKey.length) return false\n return timingSafeEqual(Buffer.from(apiKey), Buffer.from(expectedKey))\n}\n","/**\n * Sanitize a redirect path to prevent open redirect attacks.\n * Returns '/' if the path is unsafe.\n */\nexport function sanitizeRedirect(raw: string | null | undefined): string {\n if (!raw) return '/'\n if (\n raw.startsWith('/') &&\n !raw.startsWith('//') &&\n !raw.includes('@') &&\n !raw.includes('\\\\')\n ) {\n return raw\n }\n return '/'\n}\n\n/**\n * Escape HTML special characters to prevent injection in templates.\n */\nexport function escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;;;ACIzB,SAAS,iBAAiB,KAAwC;AACvE,MAAI,CAAC,IAAK,QAAO;AACjB,MACE,IAAI,WAAW,GAAG,KAClB,CAAC,IAAI,WAAW,IAAI,KACpB,CAAC,IAAI,SAAS,GAAG,KACjB,CAAC,IAAI,SAAS,IAAI,GAClB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,WAAW,GAAmB;AAC5C,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ;AAC3B;;;ADbO,SAAS,aACd,SACA,aACA,aAAqB,aACZ;AACT,QAAM,SAAS,QAAQ,QAAQ,IAAI,UAAU;AAC7C,MAAI,CAAC,UAAU,CAAC,YAAa,QAAO;AACpC,MAAI,OAAO,WAAW,YAAY,OAAQ,QAAO;AACjD,SAAO,gBAAgB,OAAO,KAAK,MAAM,GAAG,OAAO,KAAK,WAAW,CAAC;AACtE;","names":[]}
@@ -6,4 +6,14 @@ declare function formatPrice(amount: number, currency?: string): string;
6
6
  declare function formatDate(date: string | Date, options?: Intl.DateTimeFormatOptions): string;
7
7
  declare function formatDateTime(date: string | Date): string;
8
8
 
9
- export { cn, formatDate, formatDateTime, formatPrice };
9
+ /**
10
+ * Sanitize a redirect path to prevent open redirect attacks.
11
+ * Returns '/' if the path is unsafe.
12
+ */
13
+ declare function sanitizeRedirect(raw: string | null | undefined): string;
14
+ /**
15
+ * Escape HTML special characters to prevent injection in templates.
16
+ */
17
+ declare function escapeHtml(s: string): string;
18
+
19
+ export { cn, escapeHtml, formatDate, formatDateTime, formatPrice, sanitizeRedirect };
@@ -35,10 +35,24 @@ function formatDateTime(date) {
35
35
  minute: "2-digit"
36
36
  });
37
37
  }
38
+
39
+ // src/utils/sanitize.ts
40
+ function sanitizeRedirect(raw) {
41
+ if (!raw) return "/";
42
+ if (raw.startsWith("/") && !raw.startsWith("//") && !raw.includes("@") && !raw.includes("\\")) {
43
+ return raw;
44
+ }
45
+ return "/";
46
+ }
47
+ function escapeHtml(s) {
48
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
49
+ }
38
50
  export {
39
51
  cn,
52
+ escapeHtml,
40
53
  formatDate,
41
54
  formatDateTime,
42
- formatPrice
55
+ formatPrice,
56
+ sanitizeRedirect
43
57
  };
44
58
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/format.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","export function formatPrice(amount: number, currency: string = 'GBP'): string {\n return new Intl.NumberFormat('en-GB', {\n style: 'currency',\n currency,\n }).format(amount)\n}\n\nexport function formatDate(\n date: string | Date,\n options?: Intl.DateTimeFormatOptions\n): string {\n const d = typeof date === 'string' ? new Date(date) : date\n return d.toLocaleDateString(\n 'en-GB',\n options ?? {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n }\n )\n}\n\nexport function formatDateTime(date: string | Date): string {\n const d = typeof date === 'string' ? new Date(date) : date\n return d.toLocaleString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n })\n}\n"],"mappings":";;;AAAA,SAAS,YAA6B;AACtC,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;ACLO,SAAS,YAAY,QAAgB,WAAmB,OAAe;AAC5E,SAAO,IAAI,KAAK,aAAa,SAAS;AAAA,IACpC,OAAO;AAAA,IACP;AAAA,EACF,CAAC,EAAE,OAAO,MAAM;AAClB;AAEO,SAAS,WACd,MACA,SACQ;AACR,QAAM,IAAI,OAAO,SAAS,WAAW,IAAI,KAAK,IAAI,IAAI;AACtD,SAAO,EAAE;AAAA,IACP;AAAA,IACA,WAAW;AAAA,MACT,KAAK;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,eAAe,MAA6B;AAC1D,QAAM,IAAI,OAAO,SAAS,WAAW,IAAI,KAAK,IAAI,IAAI;AACtD,SAAO,EAAE,eAAe,SAAS;AAAA,IAC/B,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/cn.ts","../../src/utils/format.ts","../../src/utils/sanitize.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","export function formatPrice(amount: number, currency: string = 'GBP'): string {\n return new Intl.NumberFormat('en-GB', {\n style: 'currency',\n currency,\n }).format(amount)\n}\n\nexport function formatDate(\n date: string | Date,\n options?: Intl.DateTimeFormatOptions\n): string {\n const d = typeof date === 'string' ? new Date(date) : date\n return d.toLocaleDateString(\n 'en-GB',\n options ?? {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n }\n )\n}\n\nexport function formatDateTime(date: string | Date): string {\n const d = typeof date === 'string' ? new Date(date) : date\n return d.toLocaleString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n })\n}\n","/**\n * Sanitize a redirect path to prevent open redirect attacks.\n * Returns '/' if the path is unsafe.\n */\nexport function sanitizeRedirect(raw: string | null | undefined): string {\n if (!raw) return '/'\n if (\n raw.startsWith('/') &&\n !raw.startsWith('//') &&\n !raw.includes('@') &&\n !raw.includes('\\\\')\n ) {\n return raw\n }\n return '/'\n}\n\n/**\n * Escape HTML special characters to prevent injection in templates.\n */\nexport function escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n"],"mappings":";;;AAAA,SAAS,YAA6B;AACtC,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;ACLO,SAAS,YAAY,QAAgB,WAAmB,OAAe;AAC5E,SAAO,IAAI,KAAK,aAAa,SAAS;AAAA,IACpC,OAAO;AAAA,IACP;AAAA,EACF,CAAC,EAAE,OAAO,MAAM;AAClB;AAEO,SAAS,WACd,MACA,SACQ;AACR,QAAM,IAAI,OAAO,SAAS,WAAW,IAAI,KAAK,IAAI,IAAI;AACtD,SAAO,EAAE;AAAA,IACP;AAAA,IACA,WAAW;AAAA,MACT,KAAK;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,eAAe,MAA6B;AAC1D,QAAM,IAAI,OAAO,SAAS,WAAW,IAAI,KAAK,IAAI,IAAI;AACtD,SAAO,EAAE,eAAe,SAAS;AAAA,IAC/B,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EACV,CAAC;AACH;;;AC3BO,SAAS,iBAAiB,KAAwC;AACvE,MAAI,CAAC,IAAK,QAAO;AACjB,MACE,IAAI,WAAW,GAAG,KAClB,CAAC,IAAI,WAAW,IAAI,KACpB,CAAC,IAAI,SAAS,GAAG,KACjB,CAAC,IAAI,SAAS,IAAI,GAClB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,WAAW,GAAmB;AAC5C,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ;AAC3B;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braintwopoint0/playback-commons",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Shared UI components, auth utilities, and Supabase client for the PLAYBACK Sports ecosystem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,6 +25,10 @@
25
25
  "./playerdata": {
26
26
  "import": "./dist/playerdata/index.js",
27
27
  "types": "./dist/playerdata/index.d.ts"
28
+ },
29
+ "./security": {
30
+ "import": "./dist/security/index.js",
31
+ "types": "./dist/security/index.d.ts"
28
32
  }
29
33
  },
30
34
  "files": [
@@ -80,4 +84,4 @@
80
84
  "publishConfig": {
81
85
  "access": "public"
82
86
  }
83
- }
87
+ }
@@ -1,171 +0,0 @@
1
- declare const PLAYERDATA_BASE_URL = "https://app.playerdata.co.uk";
2
- declare const PLAYERDATA_AUTH_URL = "https://app.playerdata.co.uk/api/oauth/authorize";
3
- declare const PLAYERDATA_TOKEN_URL = "https://app.playerdata.co.uk/api/oauth/token";
4
- declare const PLAYERDATA_GRAPHQL_URL = "https://app.playerdata.co.uk/api/graphql";
5
-
6
- interface PlayerDataCredentials {
7
- clientId: string;
8
- clientSecret: string;
9
- }
10
- interface PlayerDataTokenResponse {
11
- access_token: string;
12
- refresh_token: string;
13
- token_type: string;
14
- expires_in: number;
15
- scope?: string;
16
- }
17
- interface PlayerDataToken {
18
- accessToken: string;
19
- refreshToken: string;
20
- expiresAt: number;
21
- }
22
- interface GraphQLResponse<T = unknown> {
23
- data?: T;
24
- errors?: GraphQLError[];
25
- }
26
- interface GraphQLError {
27
- message: string;
28
- locations?: {
29
- line: number;
30
- column: number;
31
- }[];
32
- path?: string[];
33
- extensions?: Record<string, unknown>;
34
- }
35
- interface PDOrganisation {
36
- id: string;
37
- name: string;
38
- clubs: PDClub[];
39
- }
40
- interface PDClub {
41
- id: string;
42
- name: string;
43
- initials?: string;
44
- sportDefinition?: PDSportDefinition;
45
- athletes?: PDAthlete[];
46
- }
47
- interface PDSportDefinition {
48
- id: string;
49
- name: string;
50
- positionDefinitions?: PDPositionDefinition[];
51
- }
52
- interface PDPositionDefinition {
53
- id: string;
54
- name: string;
55
- initial?: string;
56
- }
57
- interface PDAthlete {
58
- id: string;
59
- name: string;
60
- initials: string;
61
- customId?: string;
62
- topSpeedKph?: number;
63
- autoTopSpeedKph?: number;
64
- defaultPosition?: PDPositionDefinition;
65
- profilePicture?: PDProfilePicture;
66
- }
67
- interface PDProfilePicture {
68
- thumbnailUrl?: string;
69
- }
70
- interface PDPerson {
71
- id: string;
72
- name: string;
73
- email?: string;
74
- athletes?: PDAthlete[];
75
- clubs?: PDClub[];
76
- }
77
- interface PDSessionBase {
78
- id: string;
79
- startTime?: string;
80
- endTime?: string;
81
- description?: string;
82
- club?: {
83
- id: string;
84
- name: string;
85
- };
86
- sessionParticipations?: PDSessionParticipation[];
87
- }
88
- interface PDTrainingSession extends PDSessionBase {
89
- __typename: 'TrainingSession';
90
- }
91
- interface PDMatchSession extends PDSessionBase {
92
- __typename: 'MatchSession';
93
- opponent?: string;
94
- opponentScore?: number;
95
- ourTeam: string;
96
- }
97
- type PDSession = PDTrainingSession | PDMatchSession;
98
- interface PDSessionParticipation {
99
- id: string;
100
- athlete: {
101
- id: string;
102
- name: string;
103
- initials: string;
104
- };
105
- configuredMetrics?: PDConfiguredMetrics;
106
- }
107
- interface PDConfiguredMetrics {
108
- data?: PDGenericMetric[];
109
- }
110
- interface PDConfiguredAggMetrics {
111
- data?: PDGenericMetric[];
112
- }
113
- interface PDGenericMetric {
114
- key: string;
115
- label?: string;
116
- shortLabel?: string;
117
- localValue?: number | string | null;
118
- localUnitLabel?: string;
119
- category?: string;
120
- precision?: number;
121
- }
122
- interface PDAthleteMetricsSummary {
123
- athlete: {
124
- id: string;
125
- name: string;
126
- };
127
- configuredAggMetrics?: PDConfiguredAggMetrics;
128
- }
129
-
130
- /**
131
- * Generate the OAuth authorization URL for user-level access.
132
- * Redirect the user's browser to this URL to start the OAuth flow.
133
- */
134
- declare function generateAuthUrl(credentials: PlayerDataCredentials, redirectUri: string, state?: string): string;
135
- /**
136
- * Exchange an authorization code for access + refresh tokens.
137
- * Used after the user is redirected back from PlayerData.
138
- */
139
- declare function exchangeCode(credentials: PlayerDataCredentials, code: string, redirectUri: string): Promise<PlayerDataToken>;
140
- /**
141
- * Refresh an expired access token using a refresh token.
142
- * IMPORTANT: PlayerData refresh tokens are single-use.
143
- * The response contains a NEW refresh token that must replace the old one.
144
- */
145
- declare function refreshToken(credentials: PlayerDataCredentials, currentRefreshToken: string): Promise<PlayerDataToken>;
146
- /**
147
- * Get a service-level token using client credentials grant.
148
- * Used for org-wide data access without user interaction.
149
- */
150
- declare function getServiceToken(credentials: PlayerDataCredentials): Promise<PlayerDataToken>;
151
-
152
- /** Verify auth and get the current user's identity */
153
- declare const GET_CURRENT_PERSON = "\n query GetCurrentPerson {\n currentPerson {\n id\n name\n email\n athletes {\n id\n name\n club {\n id\n name\n }\n }\n clubs {\n id\n name\n }\n }\n }\n";
154
- /** Get club info and its athlete roster */
155
- declare const GET_CLUB = "\n query GetClub($clubId: ID!) {\n club(id: $clubId) {\n id\n name\n sportDefinition {\n id\n name\n positionDefinitions {\n id\n name\n initial\n }\n }\n athletes {\n id\n name\n initials\n customId\n topSpeedKph\n autoTopSpeedKph\n defaultPosition {\n id\n name\n initial\n }\n profilePicture {\n thumbnailUrl\n }\n }\n }\n }\n";
156
- /** List sessions for a club with optional date filters */
157
- declare const GET_CLUB_SESSIONS = "\n query GetClubSessions($clubId: ID!, $startTimeGteq: ISO8601DateTime, $startTimeLteq: ISO8601DateTime, $limit: Int, $offset: Int!) {\n club(id: $clubId) {\n id\n sessions(\n filter: { startTimeGteq: $startTimeGteq, startTimeLteq: $startTimeLteq }\n limit: $limit\n offset: $offset\n ) {\n id\n startTime\n endTime\n description\n ... on TrainingSession {\n __typename\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n }\n }\n }\n }\n";
158
- /** Get an athlete's sessions */
159
- declare const GET_ATHLETE_SESSIONS = "\n query GetAthleteSessions($athleteId: ID!, $limit: Int, $offset: Int) {\n athlete(id: $athleteId) {\n id\n name\n sessions(limit: $limit, offset: $offset) {\n id\n startTime\n endTime\n description\n ... on TrainingSession {\n __typename\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n }\n }\n }\n }\n";
160
- /** Get a session with per-athlete metrics via participations */
161
- declare const GET_SESSION_DETAILS = "\n query GetSessionDetails($sessionId: ID!) {\n session(id: $sessionId) {\n id\n startTime\n endTime\n description\n club {\n id\n name\n }\n ... on TrainingSession {\n __typename\n sessionParticipations {\n id\n athlete {\n id\n name\n initials\n }\n configuredMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n ... on MatchSession {\n __typename\n opponent\n opponentScore\n ourTeam\n sessionParticipations {\n id\n athlete {\n id\n name\n initials\n }\n configuredMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n }\n }\n";
162
- /** Get aggregated metrics for an athlete over a date range */
163
- declare const GET_ATHLETE_SUMMARY = "\n query GetAthleteSummary($athleteId: ID!, $startTime: ISO8601DateTime!, $endTime: ISO8601DateTime) {\n athlete(id: $athleteId) {\n id\n name\n timeSafeMetricsSummary(startTime: $startTime, endTime: $endTime) {\n athlete {\n id\n name\n }\n configuredAggMetrics {\n data {\n key\n label\n shortLabel\n localValue\n localUnitLabel\n category\n precision\n }\n }\n }\n }\n }\n";
164
-
165
- /**
166
- * Execute a GraphQL query against the PlayerData API.
167
- * Stateless — caller provides the access token.
168
- */
169
- declare function executeQuery<T = unknown>(token: string, query: string, variables?: Record<string, unknown>): Promise<T>;
170
-
171
- export { GET_ATHLETE_SESSIONS, GET_ATHLETE_SUMMARY, GET_CLUB, GET_CLUB_SESSIONS, GET_CURRENT_PERSON, GET_SESSION_DETAILS, type GraphQLError, type GraphQLResponse, type PDAthlete, type PDAthleteMetricsSummary, type PDClub, type PDConfiguredAggMetrics, type PDConfiguredMetrics, type PDGenericMetric, type PDMatchSession, type PDOrganisation, type PDPerson, type PDPositionDefinition, type PDProfilePicture, type PDSession, type PDSessionBase, type PDSessionParticipation, type PDSportDefinition, type PDTrainingSession, PLAYERDATA_AUTH_URL, PLAYERDATA_BASE_URL, PLAYERDATA_GRAPHQL_URL, PLAYERDATA_TOKEN_URL, type PlayerDataCredentials, type PlayerDataToken, type PlayerDataTokenResponse, exchangeCode, executeQuery, generateAuthUrl, getServiceToken, refreshToken };