@abyss-project/link 0.0.0-stage → 1.0.0

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.
Files changed (118) hide show
  1. package/.eslintrc.js +32 -0
  2. package/.prettierrc +5 -0
  3. package/README.md +21 -2
  4. package/dist/api/abyss.admin.api.d.ts +4 -0
  5. package/dist/api/abyss.admin.api.js +17 -0
  6. package/dist/api/abyss.admin.api.js.map +1 -0
  7. package/dist/api/index.d.ts +1 -0
  8. package/dist/api/index.js +18 -0
  9. package/dist/api/index.js.map +1 -0
  10. package/dist/constants/index.d.ts +1 -0
  11. package/dist/constants/index.js +18 -0
  12. package/dist/constants/index.js.map +1 -0
  13. package/dist/constants/link.constants.d.ts +14 -0
  14. package/dist/constants/link.constants.js +37 -0
  15. package/dist/constants/link.constants.js.map +1 -0
  16. package/dist/index.d.ts +33 -0
  17. package/dist/index.js +120 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/types/enum/api-error.enum.d.ts +16 -0
  20. package/dist/types/enum/api-error.enum.js +22 -0
  21. package/dist/types/enum/api-error.enum.js.map +1 -0
  22. package/dist/types/enum/index.d.ts +2 -0
  23. package/dist/types/enum/index.js +19 -0
  24. package/dist/types/enum/index.js.map +1 -0
  25. package/dist/types/enum/link.enum.d.ts +20 -0
  26. package/dist/types/enum/link.enum.js +27 -0
  27. package/dist/types/enum/link.enum.js.map +1 -0
  28. package/dist/types/index.d.ts +2 -0
  29. package/dist/types/index.js +19 -0
  30. package/dist/types/index.js.map +1 -0
  31. package/dist/types/interface/api/index.d.ts +5 -0
  32. package/dist/types/interface/api/index.js +22 -0
  33. package/dist/types/interface/api/index.js.map +1 -0
  34. package/dist/types/interface/api/requests/abyss.admin.request.d.ts +14 -0
  35. package/dist/types/interface/api/requests/abyss.admin.request.js +3 -0
  36. package/dist/types/interface/api/requests/abyss.admin.request.js.map +1 -0
  37. package/dist/types/interface/api/requests/index.d.ts +1 -0
  38. package/dist/types/interface/api/requests/index.js +18 -0
  39. package/dist/types/interface/api/requests/index.js.map +1 -0
  40. package/dist/types/interface/api/responses/abyss.admin.response.d.ts +14 -0
  41. package/dist/types/interface/api/responses/abyss.admin.response.js +3 -0
  42. package/dist/types/interface/api/responses/abyss.admin.response.js.map +1 -0
  43. package/dist/types/interface/api/responses/index.d.ts +1 -0
  44. package/dist/types/interface/api/responses/index.js +18 -0
  45. package/dist/types/interface/api/responses/index.js.map +1 -0
  46. package/dist/types/interface/api/type-message/api-error.d.ts +6 -0
  47. package/dist/types/interface/api/type-message/api-error.js +3 -0
  48. package/dist/types/interface/api/type-message/api-error.js.map +1 -0
  49. package/dist/types/interface/api/type-message/base-paginate.d.ts +10 -0
  50. package/dist/types/interface/api/type-message/base-paginate.js +3 -0
  51. package/dist/types/interface/api/type-message/base-paginate.js.map +1 -0
  52. package/dist/types/interface/api/type-message/response.d.ts +5 -0
  53. package/dist/types/interface/api/type-message/response.js +3 -0
  54. package/dist/types/interface/api/type-message/response.js.map +1 -0
  55. package/dist/types/interface/index.d.ts +2 -0
  56. package/dist/types/interface/index.js +19 -0
  57. package/dist/types/interface/index.js.map +1 -0
  58. package/dist/types/interface/models/index.d.ts +4 -0
  59. package/dist/types/interface/models/index.js +21 -0
  60. package/dist/types/interface/models/index.js.map +1 -0
  61. package/dist/types/interface/models/link.dto.d.ts +91 -0
  62. package/dist/types/interface/models/link.dto.js +3 -0
  63. package/dist/types/interface/models/link.dto.js.map +1 -0
  64. package/dist/types/interface/models/organization.dto.d.ts +11 -0
  65. package/dist/types/interface/models/organization.dto.js +3 -0
  66. package/dist/types/interface/models/organization.dto.js.map +1 -0
  67. package/dist/types/interface/models/project.dto.d.ts +12 -0
  68. package/dist/types/interface/models/project.dto.js +3 -0
  69. package/dist/types/interface/models/project.dto.js.map +1 -0
  70. package/dist/types/interface/models/user.dto.d.ts +11 -0
  71. package/dist/types/interface/models/user.dto.js +3 -0
  72. package/dist/types/interface/models/user.dto.js.map +1 -0
  73. package/dist/utils/enum.utils.d.ts +1 -0
  74. package/dist/utils/enum.utils.js +3 -0
  75. package/dist/utils/enum.utils.js.map +1 -0
  76. package/dist/utils/error.utils.d.ts +15 -0
  77. package/dist/utils/error.utils.js +25 -0
  78. package/dist/utils/error.utils.js.map +1 -0
  79. package/dist/utils/index.d.ts +3 -0
  80. package/dist/utils/index.js +20 -0
  81. package/dist/utils/index.js.map +1 -0
  82. package/dist/utils/link-slug.utils.d.ts +1 -0
  83. package/dist/utils/link-slug.utils.js +11 -0
  84. package/dist/utils/link-slug.utils.js.map +1 -0
  85. package/jest.config.cjs +9 -0
  86. package/package.json +56 -3
  87. package/src/api/abyss.admin.api.ts +39 -0
  88. package/src/api/index.ts +1 -0
  89. package/src/constants/index.ts +1 -0
  90. package/src/constants/link.constants.ts +49 -0
  91. package/src/constants/tests/link-forbidden-slugs.spec.ts +103 -0
  92. package/src/index.ts +109 -0
  93. package/src/tests/sdk-surface.spec.ts +63 -0
  94. package/src/types/enum/api-error.enum.ts +18 -0
  95. package/src/types/enum/index.ts +2 -0
  96. package/src/types/enum/link.enum.ts +34 -0
  97. package/src/types/index.ts +2 -0
  98. package/src/types/interface/api/index.ts +5 -0
  99. package/src/types/interface/api/requests/abyss.admin.request.ts +17 -0
  100. package/src/types/interface/api/requests/index.ts +1 -0
  101. package/src/types/interface/api/responses/abyss.admin.response.ts +17 -0
  102. package/src/types/interface/api/responses/index.ts +1 -0
  103. package/src/types/interface/api/type-message/api-error.ts +7 -0
  104. package/src/types/interface/api/type-message/base-paginate.ts +11 -0
  105. package/src/types/interface/api/type-message/response.ts +6 -0
  106. package/src/types/interface/index.ts +2 -0
  107. package/src/types/interface/models/index.ts +4 -0
  108. package/src/types/interface/models/link.dto.ts +117 -0
  109. package/src/types/interface/models/organization.dto.ts +21 -0
  110. package/src/types/interface/models/project.dto.ts +16 -0
  111. package/src/types/interface/models/user.dto.ts +19 -0
  112. package/src/types/tests/link-enums.spec.ts +46 -0
  113. package/src/utils/enum.utils.ts +1 -0
  114. package/src/utils/error.utils.ts +30 -0
  115. package/src/utils/index.ts +3 -0
  116. package/src/utils/link-slug.utils.ts +10 -0
  117. package/tsconfig.check.json +15 -0
  118. package/tsconfig.json +36 -0
@@ -0,0 +1,103 @@
1
+ import {
2
+ isForbiddenLinkSlug,
3
+ LINK_FORBIDDEN_SLUG_PREFIXES,
4
+ LINK_FORBIDDEN_SLUGS,
5
+ LINK_RESOURCE_PREFIXES,
6
+ LINK_SLUG_PATTERN,
7
+ LinkResourceProduct,
8
+ MAX_LINK_SLUG_LENGTH,
9
+ MIN_LINK_SLUG_LENGTH,
10
+ } from '../..';
11
+
12
+ describe('LINK_RESOURCE_PREFIXES', () => {
13
+ it('maps the eight products of spec §3.5 to their prefix', () => {
14
+ expect(LINK_RESOURCE_PREFIXES).toEqual({
15
+ [LinkResourceProduct.BEAM]: 'b',
16
+ [LinkResourceProduct.ATLAS]: 'a',
17
+ [LinkResourceProduct.DRAW]: 'd',
18
+ [LinkResourceProduct.RETROSPECTIVE]: 'r',
19
+ [LinkResourceProduct.FORM]: 'f',
20
+ [LinkResourceProduct.EXTRACT]: 'e',
21
+ [LinkResourceProduct.CLOUD]: 'c',
22
+ [LinkResourceProduct.MEMORIES]: 'm',
23
+ });
24
+ });
25
+
26
+ it('gives Spotlight no prefix: a campaign token has no Link address', () => {
27
+ expect(Object.keys(LINK_RESOURCE_PREFIXES)).not.toContain(LinkResourceProduct.SPOTLIGHT);
28
+ });
29
+
30
+ it('never gives two products the same prefix, and every prefix is lowercase', () => {
31
+ const prefixes = Object.values(LINK_RESOURCE_PREFIXES);
32
+ expect(new Set(prefixes).size).toBe(prefixes.length);
33
+ prefixes.forEach((prefix) => expect(prefix).toBe(prefix.toLowerCase()));
34
+ });
35
+ });
36
+
37
+ describe('LINK_FORBIDDEN_SLUGS', () => {
38
+ it.each(Object.values(LINK_RESOURCE_PREFIXES))('forbids the resource prefix %s', (prefix) => {
39
+ expect(LINK_FORBIDDEN_SLUGS).toContain(prefix);
40
+ expect(isForbiddenLinkSlug(prefix)).toBe(true);
41
+ expect(isForbiddenLinkSlug(prefix.toUpperCase())).toBe(true);
42
+ });
43
+
44
+ it('holds the host paths of spec §3.5, in lowercase', () => {
45
+ expect(LINK_FORBIDDEN_SLUGS).toEqual(
46
+ expect.arrayContaining([
47
+ 'api',
48
+ 'assets',
49
+ 'static',
50
+ 'health',
51
+ 'admin',
52
+ 'login',
53
+ 'favicon.ico',
54
+ 'robots.txt',
55
+ '.well-known',
56
+ ]),
57
+ );
58
+ LINK_FORBIDDEN_SLUGS.forEach((slug) => expect(slug).toBe(slug.toLowerCase()));
59
+ });
60
+
61
+ it('forbids every slug starting with _ or abyss', () => {
62
+ expect(LINK_FORBIDDEN_SLUG_PREFIXES).toEqual(['_', 'abyss']);
63
+ });
64
+ });
65
+
66
+ describe('isForbiddenLinkSlug', () => {
67
+ it.each([
68
+ '_x',
69
+ '_',
70
+ 'API',
71
+ 'Api',
72
+ 'Abyss-team',
73
+ 'ABYSS',
74
+ 'abyssal',
75
+ '.well-known',
76
+ 'Favicon.ico',
77
+ 'LOGIN',
78
+ ])('forbids %s', (slug) => {
79
+ expect(isForbiddenLinkSlug(slug)).toBe(true);
80
+ });
81
+
82
+ it.each(['abc', 'my-link', 'apis', 'x_abyss', 'B2B', 'admins'])('allows %s', (slug) => {
83
+ expect(isForbiddenLinkSlug(slug)).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe('LINK_SLUG_PATTERN', () => {
88
+ it('accepts 3 to 50 characters of [a-zA-Z0-9_-]', () => {
89
+ expect(MIN_LINK_SLUG_LENGTH).toBe(3);
90
+ expect(MAX_LINK_SLUG_LENGTH).toBe(50);
91
+ expect(LINK_SLUG_PATTERN.test('a'.repeat(MIN_LINK_SLUG_LENGTH))).toBe(true);
92
+ expect(LINK_SLUG_PATTERN.test('a'.repeat(MAX_LINK_SLUG_LENGTH))).toBe(true);
93
+ expect(LINK_SLUG_PATTERN.test('My_Link-42')).toBe(true);
94
+ });
95
+
96
+ it('refuses a slug too short, too long or with another character', () => {
97
+ expect(LINK_SLUG_PATTERN.test('a'.repeat(MIN_LINK_SLUG_LENGTH - 1))).toBe(false);
98
+ expect(LINK_SLUG_PATTERN.test('a'.repeat(MAX_LINK_SLUG_LENGTH + 1))).toBe(false);
99
+ expect(LINK_SLUG_PATTERN.test('my.link')).toBe(false);
100
+ expect(LINK_SLUG_PATTERN.test('my/link')).toBe(false);
101
+ expect(LINK_SLUG_PATTERN.test('lien-été')).toBe(false);
102
+ });
103
+ });
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ import axios from 'axios';
2
+ import axiosRetry, { IAxiosRetryConfig, isNetworkOrIdempotentRequestError } from 'axios-retry';
3
+
4
+ export * from './api';
5
+ export * from './types';
6
+ export * from './utils';
7
+ export * from './constants';
8
+
9
+ import * as abyssAdminApi from './api/abyss.admin.api';
10
+
11
+ const DEFAULT_BASE_URL = 'https://link-api.abyss-project.fr/api/';
12
+
13
+ export const API_KEY_HEADER = 'abyss-api-key';
14
+ export const ADMIN_TOKEN_HEADER = 'abyss-admin-token';
15
+
16
+ const NUMBER_RETRY_API = 10;
17
+ const BASE_DELAY_BETWEEN_RETRY = 5000;
18
+ const RETRY_CODES = [
19
+ 502, // Bad Gateway
20
+ 503, // Service Unavailable
21
+ ];
22
+
23
+ type CoreConfig = {
24
+ token?: string;
25
+ apiKey?: string;
26
+ adminToken?: string;
27
+ baseURL?: string;
28
+ };
29
+
30
+ type AbyssLinkCoreSDK = {
31
+ abyss: {
32
+ admin: {
33
+ activateUserService: typeof abyssAdminApi.activateUserServiceAbyssAdmin;
34
+ syncOrganization: typeof abyssAdminApi.syncOrganizationAbyssAdmin;
35
+ syncProject: typeof abyssAdminApi.syncProjectAbyssAdmin;
36
+ };
37
+ };
38
+ };
39
+
40
+ export class AbyssLinkCore {
41
+ private static config = {
42
+ token: '',
43
+ apiKey: '',
44
+ adminToken: '',
45
+ baseURL: DEFAULT_BASE_URL,
46
+ };
47
+ static axios = axios.create({
48
+ baseURL: this.config.baseURL,
49
+ withCredentials: true,
50
+ });
51
+
52
+ private static _sdk: AbyssLinkCoreSDK | null = null;
53
+
54
+ public static get sdk(): AbyssLinkCoreSDK {
55
+ if (!AbyssLinkCore._sdk) {
56
+ AbyssLinkCore._sdk = AbyssLinkCore.buildSDK();
57
+ }
58
+ return AbyssLinkCore._sdk;
59
+ }
60
+
61
+ private static buildSDK(): AbyssLinkCoreSDK {
62
+ return {
63
+ abyss: {
64
+ admin: {
65
+ activateUserService: abyssAdminApi.activateUserServiceAbyssAdmin,
66
+ syncOrganization: abyssAdminApi.syncOrganizationAbyssAdmin,
67
+ syncProject: abyssAdminApi.syncProjectAbyssAdmin,
68
+ },
69
+ },
70
+ };
71
+ }
72
+
73
+ public static enableAxiosRetry(axiosRetryConfig?: IAxiosRetryConfig): void {
74
+ axiosRetry(AbyssLinkCore.axios, {
75
+ retries: NUMBER_RETRY_API,
76
+ retryDelay: (retryCount) => retryCount * BASE_DELAY_BETWEEN_RETRY,
77
+ retryCondition: (error) =>
78
+ isNetworkOrIdempotentRequestError(error) ||
79
+ RETRY_CODES.includes(error.response?.status || 502),
80
+ ...axiosRetryConfig,
81
+ });
82
+ }
83
+
84
+ public static setConfig(config: CoreConfig): void {
85
+ AbyssLinkCore._sdk = null;
86
+
87
+ AbyssLinkCore.config = { ...AbyssLinkCore.config, ...config };
88
+
89
+ if (config.baseURL) {
90
+ AbyssLinkCore.axios.defaults.baseURL = config.baseURL;
91
+ }
92
+
93
+ if (config.token) {
94
+ AbyssLinkCore.axios.defaults.headers.common.Authorization = `Bearer ${config.token}`;
95
+ }
96
+
97
+ if (config.apiKey) {
98
+ AbyssLinkCore.axios.defaults.headers.common[API_KEY_HEADER] = `${config.apiKey}`;
99
+ }
100
+
101
+ if (config.adminToken) {
102
+ AbyssLinkCore.axios.defaults.headers.common[ADMIN_TOKEN_HEADER] = `${config.adminToken}`;
103
+ }
104
+ }
105
+
106
+ public static getConfig(): CoreConfig {
107
+ return AbyssLinkCore.config;
108
+ }
109
+ }
@@ -0,0 +1,63 @@
1
+ import { AbyssLinkCore, ADMIN_TOKEN_HEADER } from '..';
2
+
3
+ const USER_ID = '11111111-1111-4111-8111-111111111111';
4
+ const ORGANIZATION_ID = '22222222-2222-4222-8222-222222222222';
5
+ const PROJECT_ID = '33333333-3333-4333-8333-333333333333';
6
+
7
+ describe('AbyssLinkCore SDK surface', () => {
8
+ let post: jest.SpyInstance;
9
+
10
+ beforeEach(() => {
11
+ post = jest.spyOn(AbyssLinkCore.axios, 'post').mockResolvedValue({ data: { data: {} } });
12
+ });
13
+
14
+ afterEach(() => {
15
+ jest.restoreAllMocks();
16
+ });
17
+
18
+ it('exposes exactly the three abyss.admin calls', () => {
19
+ expect(Object.keys(AbyssLinkCore.sdk)).toEqual(['abyss']);
20
+ expect(Object.keys(AbyssLinkCore.sdk.abyss)).toEqual(['admin']);
21
+ expect(Object.keys(AbyssLinkCore.sdk.abyss.admin).sort()).toEqual([
22
+ 'activateUserService',
23
+ 'syncOrganization',
24
+ 'syncProject',
25
+ ]);
26
+ });
27
+
28
+ it('activates a user with POST abyss/admin/user/activate-service', async () => {
29
+ const body = { userId: USER_ID };
30
+ await AbyssLinkCore.sdk.abyss.admin.activateUserService(body);
31
+ expect(post).toHaveBeenCalledWith('abyss/admin/user/activate-service', body);
32
+ });
33
+
34
+ it('syncs an organization with POST abyss/admin/organization/sync', async () => {
35
+ const body = { id: ORGANIZATION_ID, name: 'Org', slug: 'org' };
36
+ await AbyssLinkCore.sdk.abyss.admin.syncOrganization(body);
37
+ expect(post).toHaveBeenCalledWith('abyss/admin/organization/sync', body);
38
+ });
39
+
40
+ it('syncs a project with POST abyss/admin/project/sync', async () => {
41
+ const body = {
42
+ id: PROJECT_ID,
43
+ name: 'Project',
44
+ createdByUserId: null,
45
+ organizationId: ORGANIZATION_ID,
46
+ };
47
+ await AbyssLinkCore.sdk.abyss.admin.syncProject(body);
48
+ expect(post).toHaveBeenCalledWith('abyss/admin/project/sync', body);
49
+ });
50
+
51
+ it('returns the response body', async () => {
52
+ post.mockResolvedValueOnce({ data: { data: { user: { id: USER_ID, isAdmin: false } } } });
53
+ await expect(
54
+ AbyssLinkCore.sdk.abyss.admin.activateUserService({ userId: USER_ID }),
55
+ ).resolves.toEqual({ data: { user: { id: USER_ID, isAdmin: false } } });
56
+ });
57
+
58
+ it('sends the admin token and the base URL given to setConfig', () => {
59
+ AbyssLinkCore.setConfig({ adminToken: 'admin-token', baseURL: 'http://localhost:10144/api/' });
60
+ expect(AbyssLinkCore.axios.defaults.headers.common[ADMIN_TOKEN_HEADER]).toBe('admin-token');
61
+ expect(AbyssLinkCore.axios.defaults.baseURL).toBe('http://localhost:10144/api/');
62
+ });
63
+ });
@@ -0,0 +1,18 @@
1
+ export enum ErrorType {
2
+ apiError = 'api_error',
3
+ authError = 'authentication_error',
4
+ resourceError = 'resource_error',
5
+ }
6
+
7
+ export enum Code {
8
+ // HTTP Error
9
+ tokenFailure = 'token_authentication_failure',
10
+ authenticationFailure = 'authentication_credentials_failure',
11
+ accessNotAllowed = 'access_not_allowed',
12
+ notFound = 'not_found',
13
+ validationError = 'resource_validation_error',
14
+ constraintError = 'resource_constraint_error',
15
+ internalServerError = 'internal_server_error',
16
+ serviceUnavailable = 'service_unavailable',
17
+ invalidData = 'invalid_data',
18
+ }
@@ -0,0 +1,2 @@
1
+ export * from './api-error.enum';
2
+ export * from './link.enum';
@@ -0,0 +1,34 @@
1
+ /** The three sorts of link (spec §2). Stored as is in `links.kind`. */
2
+ export enum LinkKind {
3
+ /** Owned by a user, served by Link at `<host>/<slug>`, billed on ABYSS_LINK. */
4
+ SHORTENED = 'SHORTENED',
5
+ /** Owned by a product, served by Link at `<host>/<prefix>/<code>`, never listed, never billed. */
6
+ RESOURCE = 'RESOURCE',
7
+ /** Owned by a user, no Link address: a token the target product (Spotlight) reads from its query string. */
8
+ CAMPAIGN_TOKEN = 'CAMPAIGN_TOKEN',
9
+ }
10
+
11
+ /** Where a visit was recorded. Stored as is in `link_visits.source`. */
12
+ export enum LinkVisitSource {
13
+ /** A 302 served by the Link host. */
14
+ REDIRECT = 'REDIRECT',
15
+ /** A page view a product reported for a campaign token. */
16
+ CAMPAIGN_VIEW = 'CAMPAIGN_VIEW',
17
+ }
18
+
19
+ /**
20
+ * The product a RESOURCE or CAMPAIGN_TOKEN link belongs to (`links.originService`).
21
+ * Link's own list, not main's `AbyssService`: a Link product is not a billing service, and
22
+ * Atlas, Draw and Retrospective are tools of AbyssWorkspace, not services.
23
+ */
24
+ export enum LinkResourceProduct {
25
+ BEAM = 'BEAM',
26
+ SPOTLIGHT = 'SPOTLIGHT',
27
+ ATLAS = 'ATLAS',
28
+ DRAW = 'DRAW',
29
+ RETROSPECTIVE = 'RETROSPECTIVE',
30
+ FORM = 'FORM',
31
+ EXTRACT = 'EXTRACT',
32
+ CLOUD = 'CLOUD',
33
+ MEMORIES = 'MEMORIES',
34
+ }
@@ -0,0 +1,2 @@
1
+ export * from './interface';
2
+ export * from './enum';
@@ -0,0 +1,5 @@
1
+ export * from './requests';
2
+ export * from './responses';
3
+ export * from './type-message/response';
4
+ export * from './type-message/api-error';
5
+ export * from './type-message/base-paginate';
@@ -0,0 +1,17 @@
1
+ export interface IActivateUserServiceAbyssAdminBody {
2
+ userId: string;
3
+ }
4
+
5
+ export interface ISyncOrganizationAbyssAdminBody {
6
+ id: string;
7
+ name: string;
8
+ slug: string;
9
+ }
10
+
11
+ export interface ISyncProjectAbyssAdminBody {
12
+ id: string;
13
+ name: string;
14
+ /** null when the author's account was deleted: the project outlives it (#199). */
15
+ createdByUserId: string | null;
16
+ organizationId: string;
17
+ }
@@ -0,0 +1 @@
1
+ export * from './abyss.admin.request';
@@ -0,0 +1,17 @@
1
+ import { IUser, IOrganization, IProject } from '../..';
2
+ import { IResponse } from '../type-message/response';
3
+
4
+ export interface IActivateUserServiceAbyssAdminData {
5
+ user: IUser;
6
+ }
7
+ export type IActivateUserServiceAbyssAdminResponse = IResponse<IActivateUserServiceAbyssAdminData>;
8
+
9
+ export interface ISyncOrganizationAbyssAdminData {
10
+ organization: IOrganization;
11
+ }
12
+ export type ISyncOrganizationAbyssAdminResponse = IResponse<ISyncOrganizationAbyssAdminData>;
13
+
14
+ export interface ISyncProjectAbyssAdminData {
15
+ project: IProject;
16
+ }
17
+ export type ISyncProjectAbyssAdminResponse = IResponse<ISyncProjectAbyssAdminData>;
@@ -0,0 +1 @@
1
+ export * from './abyss.admin.response';
@@ -0,0 +1,7 @@
1
+ import { Code, ErrorType } from '../../../enum';
2
+
3
+ export interface IApiError {
4
+ code: Code;
5
+ type: ErrorType;
6
+ message?: string;
7
+ }
@@ -0,0 +1,11 @@
1
+ export interface BasePaginate<T> {
2
+ maxPages: number;
3
+ currentPage: number;
4
+ data: T[];
5
+ totalItems: number;
6
+ }
7
+
8
+ export interface QueryPaginate {
9
+ page?: number;
10
+ limit?: number;
11
+ }
@@ -0,0 +1,6 @@
1
+ import { IApiError } from './api-error';
2
+
3
+ export interface IResponse<T> {
4
+ data: T;
5
+ error?: IApiError | Error;
6
+ }
@@ -0,0 +1,2 @@
1
+ export * from './models';
2
+ export * from './api';
@@ -0,0 +1,4 @@
1
+ export * from './user.dto';
2
+ export * from './organization.dto';
3
+ export * from './project.dto';
4
+ export * from './link.dto';
@@ -0,0 +1,117 @@
1
+ import { LinkKind, LinkResourceProduct, LinkVisitSource } from '../../enum/link.enum';
2
+ import { IOrganization } from './organization.dto';
3
+ import { IProject } from './project.dto';
4
+ import { IUser } from './user.dto';
5
+
6
+ /** The product resource a RESOURCE or CAMPAIGN_TOKEN link belongs to (spec §4). */
7
+ export interface ILinkOrigin {
8
+ service: LinkResourceProduct;
9
+ /** Product-defined type, for example `beam_transfer`. */
10
+ resourceType: string;
11
+ resourceId: string;
12
+ }
13
+
14
+ /** A row of `links` (spec §7). Which address columns are set depends on `kind`. */
15
+ export interface ILink {
16
+ id: string;
17
+ kind: LinkKind;
18
+ /** SHORTENED only, case kept as typed. */
19
+ slug: string | null;
20
+ /** SHORTENED only: `lower(slug)`, unique, the resolution key. */
21
+ slugKey: string | null;
22
+ /** RESOURCE only: a value of `LINK_RESOURCE_PREFIXES`. */
23
+ prefix: string | null;
24
+ /** RESOURCE only: base62, `LINK_RESOURCE_CODE_LENGTH` characters. */
25
+ code: string | null;
26
+ /** CAMPAIGN_TOKEN only: base62, `LINK_CAMPAIGN_TOKEN_LENGTH` characters. */
27
+ token: string | null;
28
+ targetUrl: string;
29
+ title: string | null;
30
+ description: string | null;
31
+ userId: string | null;
32
+ projectId: string | null;
33
+ organizationId: string | null;
34
+ createdByUserId: string | null;
35
+ /** Null for SHORTENED, set for RESOURCE and CAMPAIGN_TOKEN. */
36
+ originService: LinkResourceProduct | null;
37
+ originResourceType: string | null;
38
+ originResourceId: string | null;
39
+ isActive: boolean;
40
+ expiresAt: Date | null;
41
+ maxClicks: number | null;
42
+ /** SHORTENED only (scrypt, ADR-0238). */
43
+ passwordHash: string | null;
44
+ forwardCampaignParams: boolean;
45
+ isProtected: boolean;
46
+ protectedAt: Date | null;
47
+ protectedByAdminId: string | null;
48
+ /** Display values refreshed by the daily rollup; never read to decide a resolution. */
49
+ clickCount: number;
50
+ uniqueClickCount: number;
51
+ lastVisitedAt: Date | null;
52
+ createdAt?: Date;
53
+ updatedAt?: Date;
54
+
55
+ user?: IUser | null;
56
+ project?: IProject | null;
57
+ organization?: IOrganization | null;
58
+ }
59
+
60
+ /** A row of `link_visits` (spec §7). No IP, no full referer, no raw user agent. */
61
+ export interface ILinkVisit {
62
+ id: string;
63
+ linkId: string;
64
+ visitedAt: Date;
65
+ source: LinkVisitSource;
66
+ /** `hashVisitor` of the visit's UTC day: comparable within one day only. */
67
+ visitorHash: string;
68
+ isBot: boolean;
69
+ isFirstOfDay: boolean;
70
+ country: string | null;
71
+ city: string | null;
72
+ device: string | null;
73
+ browser: string | null;
74
+ os: string | null;
75
+ language: string | null;
76
+ refererDomain: string | null;
77
+ utmSource: string | null;
78
+ utmMedium: string | null;
79
+ utmCampaign: string | null;
80
+ utmTerm: string | null;
81
+ utmContent: string | null;
82
+ }
83
+
84
+ /** Visit counts by value, human visits only (spec §3.4). */
85
+ export interface ILinkBreakdowns {
86
+ countries: Record<string, number>;
87
+ cities: Record<string, number>;
88
+ devices: Record<string, number>;
89
+ browsers: Record<string, number>;
90
+ oses: Record<string, number>;
91
+ languages: Record<string, number>;
92
+ refererDomains: Record<string, number>;
93
+ utmSources: Record<string, number>;
94
+ utmMediums: Record<string, number>;
95
+ utmCampaigns: Record<string, number>;
96
+ }
97
+
98
+ /** A row of `link_daily_stats`: one per link and UTC day (spec §3.4). */
99
+ export interface ILinkDailyStats extends ILinkBreakdowns {
100
+ linkId: string;
101
+ /** UTC day, `YYYY-MM-DD`. */
102
+ day: string;
103
+ /** Human visits. */
104
+ visits: number;
105
+ /** Distinct visitor hashes of the day. */
106
+ uniqueVisitors: number;
107
+ botVisits: number;
108
+ }
109
+
110
+ /** Statistics of a link over a period. `uniqueVisitors` is the sum of the daily distinct visitors. */
111
+ export interface ILinkStats extends ILinkBreakdowns {
112
+ visits: number;
113
+ uniqueVisitors: number;
114
+ botVisits: number;
115
+ lastVisitedAt: Date | null;
116
+ daily: ILinkDailyStats[];
117
+ }
@@ -0,0 +1,21 @@
1
+ import { IProject } from './project.dto';
2
+
3
+ export interface IOrganization {
4
+ id?: string;
5
+ name: string;
6
+ slug: string;
7
+ // Mirror of abyss-main Organization.kind. Drives the family default-share
8
+ // permission (family → EDIT, else VIEW). Synced by organization.middleware.ts.
9
+ // Typed as a literal union that mirrors @abyss-project/main's OrganizationKind
10
+ // enum values 1:1 — this published SDK package intentionally does NOT depend on
11
+ // @abyss-project/main, so the enum is referenced by value (the api-side model
12
+ // and middleware narrow this to OrganizationKind, where main IS resolvable).
13
+ kind?: 'standard' | 'family';
14
+ // Last refresh of `kind` from abyss-main; drives the TTL (≤5min) sync gate.
15
+ kindSyncedAt?: string | null;
16
+ updatedAt?: Date;
17
+ createdAt?: Date;
18
+
19
+ // Associations
20
+ projects?: IProject[];
21
+ }
@@ -0,0 +1,16 @@
1
+ import { IOrganization } from './organization.dto';
2
+ import { IUser } from './user.dto';
3
+
4
+ export interface IProject {
5
+ id?: string;
6
+ name: string;
7
+ // Historical creator of the project, null once their account is deleted. NOT used for access control.
8
+ createdByUserId: string | null;
9
+ organizationId: string;
10
+ updatedAt?: Date;
11
+ createdAt?: Date;
12
+
13
+ // Associations
14
+ organization?: IOrganization;
15
+ user?: IUser;
16
+ }
@@ -0,0 +1,19 @@
1
+ import { IProject } from './project.dto';
2
+
3
+ export interface IUser {
4
+ id?: string;
5
+ isAdmin: boolean;
6
+ // Personal API key mirror from abyss-main (ADR-0037). The lookup hash is the
7
+ // non-reversible sha256 fingerprint used for local validation; revokedAt marks
8
+ // a mirrored key that must no longer authenticate.
9
+ personalApiKeyLookupHash?: string | null;
10
+ personalApiKeyRevokedAt?: Date | null;
11
+ // Set when the account was deleted in abyss-main (USER_DELETED, #199): the row stays, marked,
12
+ // and a session token signed before the deletion no longer authenticates.
13
+ accountDeletedAt?: Date | null;
14
+ updatedAt?: Date;
15
+ createdAt?: Date;
16
+
17
+ // Associations
18
+ projects?: IProject[];
19
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ LINK_CAMPAIGN_TOKEN_LENGTH,
3
+ LINK_CAMPAIGN_TOKENS_PER_PROJECT,
4
+ LINK_RESOURCE_CODE_LENGTH,
5
+ LINK_VISIT_RETENTION_DAYS,
6
+ LinkKind,
7
+ LinkResourceProduct,
8
+ LinkVisitSource,
9
+ MAX_LINK_PASSWORD_LENGTH,
10
+ MIN_LINK_PASSWORD_LENGTH,
11
+ } from '../..';
12
+
13
+ describe('link enums: stored values', () => {
14
+ it('LinkKind has exactly the three kinds of spec §2', () => {
15
+ expect(Object.values(LinkKind)).toEqual(['SHORTENED', 'RESOURCE', 'CAMPAIGN_TOKEN']);
16
+ });
17
+
18
+ it('LinkVisitSource has exactly REDIRECT and CAMPAIGN_VIEW', () => {
19
+ expect(Object.values(LinkVisitSource)).toEqual(['REDIRECT', 'CAMPAIGN_VIEW']);
20
+ });
21
+
22
+ it('LinkResourceProduct has exactly the products of spec §3.5 and Spotlight', () => {
23
+ expect(Object.values(LinkResourceProduct)).toEqual([
24
+ 'BEAM',
25
+ 'SPOTLIGHT',
26
+ 'ATLAS',
27
+ 'DRAW',
28
+ 'RETROSPECTIVE',
29
+ 'FORM',
30
+ 'EXTRACT',
31
+ 'CLOUD',
32
+ 'MEMORIES',
33
+ ]);
34
+ });
35
+ });
36
+
37
+ describe('link limits', () => {
38
+ it('carries the values of the spec', () => {
39
+ expect(MIN_LINK_PASSWORD_LENGTH).toBe(4);
40
+ expect(MAX_LINK_PASSWORD_LENGTH).toBe(128);
41
+ expect(LINK_RESOURCE_CODE_LENGTH).toBe(10);
42
+ expect(LINK_CAMPAIGN_TOKEN_LENGTH).toBe(12);
43
+ expect(LINK_CAMPAIGN_TOKENS_PER_PROJECT).toBe(50);
44
+ expect(LINK_VISIT_RETENTION_DAYS).toBe(90);
45
+ });
46
+ });
@@ -0,0 +1 @@
1
+ export type ValueOf<T> = T[keyof T];